loading
Generated 2024-07-16T17:35:48+05:30

All Files ( 35.54% covered at 260.55 hits/line )

448 files in total.
14744 relevant lines, 5240 lines covered and 9504 lines missed. ( 35.54% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/admin/demos.rb 0.00 % 27 23 0 23 0.00
app/admin/forward_email_rules.rb 0.00 % 20 17 0 17 0.00
app/admin/groups.rb 0.00 % 281 243 0 243 0.00
app/admin/subscriptions.rb 0.00 % 101 93 0 93 0.00
app/admin/users.rb 0.00 % 184 159 0 159 0.00
app/controllers/api/b2/base_controller.rb 100.00 % 27 18 18 0 10.94
app/controllers/api/b2/discussions_controller.rb 75.00 % 12 8 6 2 1.63
app/controllers/api/b2/memberships_controller.rb 85.19 % 58 27 23 4 4.48
app/controllers/api/b2/polls_controller.rb 80.00 % 16 10 8 2 2.40
app/controllers/api/b3/users_controller.rb 100.00 % 22 15 15 0 1.93
app/controllers/api/v1/announcements_controller.rb 68.92 % 187 74 51 23 11.22
app/controllers/api/v1/attachments_controller.rb 91.67 % 37 24 22 2 0.96
app/controllers/api/v1/boot_controller.rb 0.00 % 39 35 0 35 0.00
app/controllers/api/v1/chatbots_controller.rb 0.00 % 16 14 0 14 0.00
app/controllers/api/v1/comments_controller.rb 46.67 % 21 15 7 8 0.60
app/controllers/api/v1/contact_messages_controller.rb 0.00 % 2 2 0 2 0.00
app/controllers/api/v1/demos_controller.rb 0.00 % 19 16 0 16 0.00
app/controllers/api/v1/discussion_readers_controller.rb 0.00 % 59 51 0 51 0.00
app/controllers/api/v1/discussion_templates_controller.rb 0.00 % 142 118 0 118 0.00
app/controllers/api/v1/discussions_controller.rb 75.73 % 187 103 78 25 2.69
app/controllers/api/v1/documents_controller.rb 82.61 % 56 23 19 4 2.43
app/controllers/api/v1/events_controller.rb 65.22 % 115 69 45 24 6.30
app/controllers/api/v1/group_surveys_controller.rb 0.00 % 7 6 0 6 0.00
app/controllers/api/v1/groups_controller.rb 60.00 % 73 45 27 18 0.96
app/controllers/api/v1/identities_controller.rb 0.00 % 26 21 0 21 0.00
app/controllers/api/v1/link_previews_controller.rb 0.00 % 17 14 0 14 0.00
app/controllers/api/v1/login_tokens_controller.rb 100.00 % 12 8 8 0 2.25
app/controllers/api/v1/membership_requests_controller.rb 86.36 % 37 22 19 3 1.23
app/controllers/api/v1/memberships_controller.rb 78.69 % 103 61 48 13 1.90
app/controllers/api/v1/notifications_controller.rb 0.00 % 17 15 0 15 0.00
app/controllers/api/v1/outcomes_controller.rb 100.00 % 9 5 5 0 5.60
app/controllers/api/v1/poll_templates_controller.rb 0.00 % 105 91 0 91 0.00
app/controllers/api/v1/polls_controller.rb 78.38 % 60 37 29 8 1.97
app/controllers/api/v1/profile_controller.rb 70.21 % 174 94 66 28 1.87
app/controllers/api/v1/reactions_controller.rb 86.36 % 37 22 19 3 2.00
app/controllers/api/v1/received_emails_controller.rb 86.67 % 57 30 26 4 1.10
app/controllers/api/v1/registrations_controller.rb 96.67 % 52 30 29 1 4.43
app/controllers/api/v1/reports_controller.rb 0.00 % 56 51 0 51 0.00
app/controllers/api/v1/restful_controller.rb 94.12 % 22 17 16 1 1.18
app/controllers/api/v1/search_controller.rb 85.96 % 119 57 49 8 8.49
app/controllers/api/v1/sessions_controller.rb 65.79 % 63 38 25 13 2.53
app/controllers/api/v1/snorlax_base.rb 91.67 % 289 156 143 13 76.98
app/controllers/api/v1/stances_controller.rb 86.67 % 138 75 65 10 2.63
app/controllers/api/v1/tags_controller.rb 55.00 % 36 20 11 9 0.55
app/controllers/api/v1/tasks_controller.rb 100.00 % 48 22 22 0 1.41
app/controllers/api/v1/translations_controller.rb 0.00 % 6 6 0 6 0.00
app/controllers/api/v1/trials_controller.rb 100.00 % 34 22 22 0 1.00
app/controllers/api/v1/versions_controller.rb 100.00 % 29 13 13 0 1.23
app/controllers/application_controller.rb 0.00 % 184 152 0 152 0.00
app/controllers/authenticate_by_unsubscribe_token_controller.rb 87.50 % 15 8 7 1 3.13
app/controllers/dev/base_controller.rb 0.00 % 31 26 0 26 0.00
app/controllers/dev/discussions_controller.rb 0.00 % 41 36 0 36 0.00
app/controllers/dev/nightwatch_controller.rb 0.00 % 38 34 0 34 0.00
app/controllers/dev/polls_controller.rb 0.00 % 136 103 0 103 0.00
app/controllers/dev/scenarios/auth.rb 0.00 % 163 135 0 135 0.00
app/controllers/dev/scenarios/dashboard.rb 0.00 % 23 20 0 20 0.00
app/controllers/dev/scenarios/discussion.rb 0.00 % 216 187 0 187 0.00
app/controllers/dev/scenarios/email_settings.rb 0.00 % 12 11 0 11 0.00
app/controllers/dev/scenarios/group.rb 0.00 % 226 203 0 203 0.00
app/controllers/dev/scenarios/inbox.rb 0.00 % 8 8 0 8 0.00
app/controllers/dev/scenarios/join_group.rb 0.00 % 18 17 0 17 0.00
app/controllers/dev/scenarios/membership.rb 0.00 % 13 12 0 12 0.00
app/controllers/dev/scenarios/membership_request.rb 0.00 % 11 11 0 11 0.00
app/controllers/dev/scenarios/notification.rb 0.00 % 14 13 0 13 0.00
app/controllers/dev/scenarios/profile.rb 0.00 % 29 26 0 26 0.00
app/controllers/dev/scenarios/tags.rb 0.00 % 34 31 0 31 0.00
app/controllers/dev/scenarios/util.rb 0.00 % 32 29 0 29 0.00
app/controllers/direct_uploads_controller.rb 0.00 % 27 24 0 24 0.00
app/controllers/discussions_controller.rb 100.00 % 2 1 1 0 1.00
app/controllers/email_actions_controller.rb 87.50 % 64 32 28 4 1.56
app/controllers/groups_controller.rb 82.35 % 26 17 14 3 0.94
app/controllers/help_controller.rb 0.00 % 20 17 0 17 0.00
app/controllers/identities/base_controller.rb 0.00 % 94 74 0 74 0.00
app/controllers/identities/facebook_controller.rb 0.00 % 35 27 0 27 0.00
app/controllers/identities/google_controller.rb 0.00 % 16 12 0 12 0.00
app/controllers/identities/nextcloud_controller.rb 0.00 % 20 15 0 15 0.00
app/controllers/identities/oauth_controller.rb 0.00 % 16 12 0 12 0.00
app/controllers/identities/saml_controller.rb 0.00 % 64 36 0 36 0.00
app/controllers/login_tokens_controller.rb 100.00 % 13 7 7 0 1.71
app/controllers/manifest_controller.rb 100.00 % 28 9 9 0 2.11
app/controllers/memberships_controller.rb 100.00 % 33 19 19 0 3.47
app/controllers/merge_users_controller.rb 64.71 % 27 17 11 6 0.88
app/controllers/pie_chart_controller.rb 0.00 % 25 18 0 18 0.00
app/controllers/poll_templates_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/polls_controller.rb 78.95 % 33 19 15 4 1.21
app/controllers/received_emails_controller.rb 94.12 % 38 17 16 1 8.71
app/controllers/redirect_controller.rb 83.33 % 23 12 10 2 1.00
app/controllers/robots_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/root_controller.rb 0.00 % 5 5 0 5 0.00
app/controllers/thread_templates_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/users/passwords_controller.rb 0.00 % 21 16 0 16 0.00
app/controllers/users_controller.rb 100.00 % 3 2 2 0 1.00
app/extras/app_config.rb 0.00 % 119 104 0 104 0.00
app/extras/clients/base.rb 77.36 % 117 53 41 12 14.94
app/extras/clients/google.rb 0.00 % 28 21 0 21 0.00
app/extras/clients/nextcloud.rb 0.00 % 36 28 0 28 0.00
app/extras/clients/oauth.rb 0.00 % 44 36 0 36 0.00
app/extras/clients/recaptcha.rb 50.00 % 17 10 5 5 0.50
app/extras/clients/request.rb 92.31 % 21 13 12 1 12.38
app/extras/clients/slack.rb 0.00 % 57 45 0 45 0.00
app/extras/clients/webhook.rb 60.00 % 23 10 6 4 4.30
app/extras/group_exporter.rb 56.00 % 57 25 14 11 1.04
app/extras/model_locator.rb 100.00 % 40 22 22 0 231.09
app/extras/poll_exporter.rb 100.00 % 68 26 26 0 1.50
app/extras/queries/admin_group_page.rb 0.00 % 56 45 0 45 0.00
app/extras/queries/explore_groups.rb 92.86 % 30 14 13 1 8.86
app/extras/queries/group_stats.rb 0.00 % 21 17 0 17 0.00
app/extras/queries/union_query.rb 100.00 % 6 4 4 0 4.50
app/extras/queries/users_by_volume_query.rb 100.00 % 34 14 14 0 408.14
app/extras/range_set.rb 100.00 % 143 68 68 0 801.37
app/extras/time_zone_to_city.rb 100.00 % 28 16 16 0 2073.50
app/extras/user_inviter.rb 94.55 % 125 55 52 3 348.69
app/extras/username_generator.rb 95.65 % 41 23 22 1 2679.13
app/helpers/application_helper.rb 0.00 % 36 30 0 30 0.00
app/helpers/current_user_helper.rb 0.00 % 36 28 0 28 0.00
app/helpers/dev/dashboard_helper.rb 0.00 % 41 32 0 32 0.00
app/helpers/dev/fake_data_helper.rb 0.00 % 467 387 0 387 0.00
app/helpers/dev/ninties_movies_helper.rb 0.00 % 396 318 0 318 0.00
app/helpers/dev/scenarios_helper.rb 0.00 % 334 279 0 279 0.00
app/helpers/email_helper.rb 0.00 % 158 133 0 133 0.00
app/helpers/formatted_date_helper.rb 0.00 % 57 50 0 50 0.00
app/helpers/load_and_authorize.rb 0.00 % 7 7 0 7 0.00
app/helpers/locales_helper.rb 0.00 % 84 67 0 67 0.00
app/helpers/pending_actions_helper.rb 0.00 % 113 93 0 93 0.00
app/helpers/pretty_url_helper.rb 0.00 % 68 59 0 59 0.00
app/helpers/protected_from_forgery.rb 0.00 % 24 19 0 19 0.00
app/helpers/sentry_helper.rb 0.00 % 9 8 0 8 0.00
app/helpers/uses_metadata.rb 62.50 % 29 16 10 6 0.75
app/mailers/base_mailer.rb 96.15 % 42 26 25 1 351.77
app/mailers/contact_mailer.rb 0.00 % 13 12 0 12 0.00
app/mailers/event_mailer.rb 97.62 % 113 42 41 1 727.95
app/mailers/forward_mailer.rb 100.00 % 15 6 6 0 1.00
app/mailers/group_mailer.rb 100.00 % 12 6 6 0 1.67
app/mailers/task_mailer.rb 100.00 % 12 5 5 0 1.00
app/mailers/user_mailer.rb 86.54 % 120 52 45 7 2.60
app/models/ability/attachment.rb 85.71 % 13 7 6 1 734.71
app/models/ability/base.rb 94.59 % 53 37 35 2 94.89
app/models/ability/chatbot.rb 80.00 % 9 5 4 1 685.60
app/models/ability/comment.rb 100.00 % 41 18 18 0 585.50
app/models/ability/discussion.rb 91.67 % 71 36 33 3 698.44
app/models/ability/discussion_reader.rb 63.64 % 21 11 7 4 778.82
app/models/ability/discussion_template.rb 80.00 % 9 5 4 1 685.60
app/models/ability/document.rb 55.56 % 17 9 5 4 571.22
app/models/ability/event.rb 100.00 % 11 6 6 0 571.67
app/models/ability/group.rb 95.45 % 99 44 42 2 555.95
app/models/ability/identity.rb 80.00 % 9 5 4 1 685.60
app/models/ability/membership.rb 94.12 % 31 17 16 1 608.00
app/models/ability/membership_request.rb 81.82 % 19 11 9 2 626.18
app/models/ability/outcome.rb 100.00 % 26 14 14 0 763.79
app/models/ability/poll.rb 100.00 % 82 41 41 0 628.73
app/models/ability/poll_template.rb 80.00 % 9 5 4 1 685.60
app/models/ability/reaction.rb 88.89 % 17 9 8 1 762.33
app/models/ability/stance.rb 86.67 % 32 15 13 2 803.80
app/models/ability/tag.rb 87.50 % 15 8 7 1 643.50
app/models/ability/task.rb 100.00 % 9 5 5 0 686.60
app/models/ability/user.rb 100.00 % 27 13 13 0 792.46
app/models/anonymous_user.rb 72.73 % 21 11 8 3 40.00
app/models/application_record.rb 0.00 % 3 3 0 3 0.00
app/models/attachment.rb 100.00 % 2 1 1 0 1.00
app/models/blocked_domain.rb 0.00 % 2 2 0 2 0.00
app/models/boot/site.rb 0.00 % 39 38 0 38 0.00
app/models/boot/user.rb 100.00 % 30 14 14 0 3.50
app/models/calendar_invite.rb 91.30 % 35 23 21 2 5.17
app/models/chatbot.rb 88.89 % 18 9 8 1 0.89
app/models/comment.rb 93.42 % 159 76 71 5 99.66
app/models/concerns/avatar_initials.rb 0.00 % 23 15 0 15 0.00
app/models/concerns/discussion_export_relations.rb 95.00 % 32 20 19 1 0.95
app/models/concerns/events/live_update.rb 100.00 % 19 11 11 0 515.64
app/models/concerns/events/notify/author.rb 90.91 % 20 11 10 1 18.09
app/models/concerns/events/notify/by_email.rb 88.89 % 17 9 8 1 421.22
app/models/concerns/events/notify/chatbots.rb 100.00 % 6 4 4 0 451.00
app/models/concerns/events/notify/in_app.rb 100.00 % 64 27 27 0 631.89
app/models/concerns/events/notify/mentions.rb 92.86 % 26 14 13 1 224.43
app/models/concerns/group_export_relations.rb 0.00 % 145 102 0 102 0.00
app/models/concerns/group_privacy.rb 0.00 % 157 132 0 132 0.00
app/models/concerns/has_avatar.rb 0.00 % 85 75 0 75 0.00
app/models/concerns/has_created_event.rb 0.00 % 13 11 0 11 0.00
app/models/concerns/has_custom_fields.rb 0.00 % 8 8 0 8 0.00
app/models/concerns/has_defaults.rb 0.00 % 7 7 0 7 0.00
app/models/concerns/has_events.rb 0.00 % 9 8 0 8 0.00
app/models/concerns/has_experiences.rb 0.00 % 6 6 0 6 0.00
app/models/concerns/has_mentions.rb 0.00 % 59 46 0 46 0.00
app/models/concerns/has_rich_text.rb 0.00 % 139 118 0 118 0.00
app/models/concerns/has_tags.rb 0.00 % 16 13 0 13 0.00
app/models/concerns/has_timeframe.rb 0.00 % 15 11 0 11 0.00
app/models/concerns/has_tokens.rb 0.00 % 7 7 0 7 0.00
app/models/concerns/has_volume.rb 0.00 % 41 34 0 34 0.00
app/models/concerns/identities/with_client.rb 0.00 % 33 25 0 25 0.00
app/models/concerns/message_channel.rb 0.00 % 7 6 0 6 0.00
app/models/concerns/no_forbidden_emails.rb 0.00 % 8 7 0 7 0.00
app/models/concerns/no_spam.rb 0.00 % 11 8 0 8 0.00
app/models/concerns/null/group.rb 72.73 % 184 44 32 12 381.80
app/models/concerns/null/object.rb 88.24 % 63 34 30 4 14173.15
app/models/concerns/null/user.rb 85.19 % 74 27 23 4 734.81
app/models/concerns/reactable.rb 0.00 % 6 6 0 6 0.00
app/models/concerns/readable_unguessable_urls.rb 0.00 % 32 27 0 27 0.00
app/models/concerns/routing.rb 0.00 % 10 9 0 9 0.00
app/models/concerns/searchable.rb 0.00 % 25 21 0 21 0.00
app/models/concerns/self_referencing.rb 0.00 % 8 7 0 7 0.00
app/models/concerns/translatable.rb 0.00 % 28 22 0 22 0.00
app/models/concerns/uses_organisation_scope.rb 0.00 % 7 6 0 6 0.00
app/models/contact_message.rb 0.00 % 10 7 0 7 0.00
app/models/demo.rb 0.00 % 9 8 0 8 0.00
app/models/discussion.rb 94.24 % 263 139 131 8 838.88
app/models/discussion_reader.rb 93.42 % 133 76 71 5 250.13
app/models/discussion_template.rb 53.57 % 68 28 15 13 0.54
app/models/document.rb 88.57 % 59 35 31 4 67.29
app/models/event.rb 0.00 % 215 178 0 178 0.00
app/models/events/announcement_resend.rb 0.00 % 21 17 0 17 0.00
app/models/events/comment_edited.rb 100.00 % 8 5 5 0 1.80
app/models/events/comment_replied_to.rb 100.00 % 18 10 10 0 2.70
app/models/events/discussion_announced.rb 100.00 % 21 6 6 0 2.33
app/models/events/discussion_closed.rb 0.00 % 10 9 0 9 0.00
app/models/events/discussion_description_edited.rb 0.00 % 5 5 0 5 0.00
app/models/events/discussion_edited.rb 100.00 % 27 11 11 0 4.00
app/models/events/discussion_forked.rb 0.00 % 10 10 0 10 0.00
app/models/events/discussion_moved.rb 100.00 % 11 4 4 0 2.50
app/models/events/discussion_reopened.rb 0.00 % 9 8 0 8 0.00
app/models/events/discussion_title_edited.rb 0.00 % 5 5 0 5 0.00
app/models/events/group_identity_created.rb 0.00 % 12 11 0 11 0.00
app/models/events/invitation_accepted.rb 100.00 % 22 12 12 0 3.75
app/models/events/membership_created.rb 100.00 % 15 5 5 0 3.40
app/models/events/membership_request_approved.rb 100.00 % 20 12 12 0 1.08
app/models/events/membership_requested.rb 0.00 % 28 22 0 22 0.00
app/models/events/membership_resent.rb 78.57 % 28 14 11 3 0.79
app/models/events/new_comment.rb 100.00 % 25 12 12 0 61.75
app/models/events/new_coordinator.rb 100.00 % 13 7 7 0 1.29
app/models/events/new_discussion.rb 100.00 % 24 10 10 0 110.00
app/models/events/outcome_announced.rb 100.00 % 11 5 5 0 3.80
app/models/events/outcome_created.rb 100.00 % 20 8 8 0 3.88
app/models/events/outcome_review_due.rb 100.00 % 25 13 13 0 3.77
app/models/events/outcome_updated.rb 100.00 % 19 8 8 0 1.00
app/models/events/poll_announced.rb 92.31 % 37 13 12 1 1.92
app/models/events/poll_closed_by_user.rb 100.00 % 11 5 5 0 2.00
app/models/events/poll_closing_soon.rb 100.00 % 45 21 21 0 28.38
app/models/events/poll_created.rb 100.00 % 15 8 8 0 21.88
app/models/events/poll_edited.rb 100.00 % 22 8 8 0 2.50
app/models/events/poll_expired.rb 100.00 % 23 12 12 0 8.08
app/models/events/poll_option_added.rb 0.00 % 21 17 0 17 0.00
app/models/events/poll_reminder.rb 0.00 % 21 20 0 20 0.00
app/models/events/poll_reopened.rb 100.00 % 10 5 5 0 1.00
app/models/events/reaction_created.rb 100.00 % 29 14 14 0 2.79
app/models/events/stance_created.rb 86.36 % 43 22 19 3 17.86
app/models/events/stance_updated.rb 0.00 % 2 2 0 2 0.00
app/models/events/unknown_sender.rb 93.33 % 32 15 14 1 1.33
app/models/events/user_added_to_group.rb 72.73 % 19 11 8 3 0.73
app/models/events/user_joined_group.rb 0.00 % 5 5 0 5 0.00
app/models/events/user_mentioned.rb 100.00 % 19 10 10 0 11.80
app/models/events/user_reactivated.rb 0.00 % 12 9 0 9 0.00
app/models/formal_group.rb 0.00 % 2 2 0 2 0.00
app/models/forward_email_rule.rb 0.00 % 5 5 0 5 0.00
app/models/global_message_channel.rb 75.00 % 7 4 3 1 0.75
app/models/group.rb 0.00 % 476 399 0 399 0.00
app/models/group_identity.rb 92.31 % 21 13 12 1 0.92
app/models/group_survey.rb 0.00 % 5 4 0 4 0.00
app/models/guest_group.rb 0.00 % 2 2 0 2 0.00
app/models/identities/base.rb 0.00 % 37 30 0 30 0.00
app/models/identities/facebook.rb 0.00 % 32 26 0 26 0.00
app/models/identities/google.rb 0.00 % 11 10 0 10 0.00
app/models/identities/nextcloud.rb 0.00 % 11 10 0 10 0.00
app/models/identities/oauth.rb 0.00 % 10 9 0 9 0.00
app/models/identities/saml.rb 0.00 % 29 26 0 26 0.00
app/models/logged_out_user.rb 84.09 % 83 44 37 7 977.34
app/models/login_token.rb 100.00 % 31 18 18 0 11.67
app/models/member_email_alias.rb 100.00 % 8 6 6 0 3.00
app/models/membership.rb 0.00 % 95 75 0 75 0.00
app/models/membership_request.rb 94.83 % 98 58 55 3 7.64
app/models/notification.rb 100.00 % 15 11 11 0 130.55
app/models/null_discussion.rb 0.00 % 50 42 0 42 0.00
app/models/null_group.rb 100.00 % 7 4 4 0 699.50
app/models/null_poll.rb 0.00 % 54 46 0 46 0.00
app/models/outcome.rb 92.59 % 122 54 50 4 48.19
app/models/permitted_params.rb 79.17 % 254 48 38 10 11.56
app/models/poll.rb 0.00 % 524 440 0 440 0.00
app/models/poll_option.rb 100.00 % 62 24 24 0 2417.58
app/models/poll_template.rb 58.06 % 88 31 18 13 0.58
app/models/reaction.rb 78.57 % 24 14 11 3 0.86
app/models/received_email.rb 96.15 % 114 52 50 2 43.42
app/models/search_result.rb 75.00 % 31 8 6 2 0.75
app/models/site_settings.rb 0.00 % 11 11 0 11 0.00
app/models/stance.rb 89.04 % 275 146 130 16 398.42
app/models/stance_choice.rb 78.26 % 38 23 18 5 520.78
app/models/subscription.rb 0.00 % 76 63 0 63 0.00
app/models/tag.rb 0.00 % 34 13 0 13 0.00
app/models/tagging.rb 0.00 % 8 6 0 6 0.00
app/models/task.rb 100.00 % 12 8 8 0 1.13
app/models/tasks_user.rb 100.00 % 4 3 3 0 1.00
app/models/translation.rb 0.00 % 12 8 0 8 0.00
app/models/user.rb 0.00 % 391 311 0 311 0.00
app/queries/attachment_query.rb 100.00 % 45 10 10 0 1.80
app/queries/contactable_query.rb 100.00 % 34 15 15 0 7.73
app/queries/discussion_query.rb 86.96 % 64 23 20 3 60.57
app/queries/group_query.rb 100.00 % 14 7 7 0 2.71
app/queries/membership_query.rb 80.00 % 53 25 20 5 3.48
app/queries/poll_query.rb 82.76 % 61 29 24 5 70.38
app/queries/reaction_query.rb 100.00 % 41 19 19 0 1.63
app/queries/user_query.rb 100.00 % 72 28 28 0 1340.43
app/serializers/application_serializer.rb 91.36 % 160 81 74 7 7223.70
app/serializers/attachment_serializer.rb 100.00 % 37 19 19 0 1.32
app/serializers/author_serializer.rb 100.00 % 46 17 17 0 1166.71
app/serializers/chatbot_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/comment_serializer.rb 88.89 % 31 9 8 1 27.67
app/serializers/contact_message_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/contact_request_serializer.rb 0.00 % 2 2 0 2 0.00
app/serializers/contact_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/current_user_serializer.rb 81.82 % 24 11 9 2 3.00
app/serializers/demo_serializer.rb 0.00 % 12 10 0 10 0.00
app/serializers/discussion_reader_serializer.rb 100.00 % 28 9 9 0 3.33
app/serializers/discussion_serializer.rb 100.00 % 92 33 33 0 1454.33
app/serializers/discussion_template_serializer.rb 0.00 % 29 27 0 27 0.00
app/serializers/document_serializer.rb 100.00 % 17 9 9 0 6.78
app/serializers/event_serializer.rb 91.43 % 63 35 32 3 1563.94
app/serializers/group_serializer.rb 93.55 % 120 31 29 2 563.61
app/serializers/identity_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/locale_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/marked_as_read/discussion_serializer.rb 0.00 % 32 27 0 27 0.00
app/serializers/member_email_alias_serializer.rb 75.00 % 20 8 6 2 0.75
app/serializers/member_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/membership_request_serializer.rb 100.00 % 10 6 6 0 1.50
app/serializers/membership_serializer.rb 100.00 % 28 10 10 0 26.30
app/serializers/metadata/discussion_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/metadata/group_serializer.rb 0.00 % 17 14 0 14 0.00
app/serializers/metadata/poll_serializer.rb 0.00 % 15 12 0 12 0.00
app/serializers/metadata/user_serializer.rb 0.00 % 15 12 0 12 0.00
app/serializers/metadata_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/model_error_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/notification/event_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/notification_serializer.rb 95.24 % 53 21 20 1 581.14
app/serializers/outcome_serializer.rb 100.00 % 28 6 6 0 6.33
app/serializers/pending/base_serializer.rb 0.00 % 57 44 0 44 0.00
app/serializers/pending/discussion_reader_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/pending/group_serializer.rb 0.00 % 39 30 0 30 0.00
app/serializers/pending/identity_serializer.rb 0.00 % 21 18 0 18 0.00
app/serializers/pending/membership_serializer.rb 0.00 % 43 32 0 32 0.00
app/serializers/pending/stance_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/pending/token_serializer.rb 0.00 % 38 30 0 30 0.00
app/serializers/pending/user_serializer.rb 0.00 % 17 13 0 13 0.00
app/serializers/permitted_params_serializer.rb 0.00 % 12 9 0 9 0.00
app/serializers/plugin_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/poll_option_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/poll_serializer.rb 96.88 % 142 32 31 1 220.19
app/serializers/poll_template_serializer.rb 0.00 % 46 44 0 44 0.00
app/serializers/reaction_serializer.rb 100.00 % 4 3 3 0 1.00
app/serializers/received_email_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/restricted/group_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/restricted/membership_serializer.rb 0.00 % 5 5 0 5 0.00
app/serializers/restricted/user_serializer.rb 0.00 % 10 9 0 9 0.00
app/serializers/search_result_serializer.rb 100.00 % 23 4 4 0 1.00
app/serializers/search_results/base_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/search_results/comment_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/search_results/discussion_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/stance_choice_serializer.rb 0.00 % 24 19 0 19 0.00
app/serializers/stance_serializer.rb 96.67 % 75 30 29 1 96.63
app/serializers/tag_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/task_serializer.rb 100.00 % 14 4 4 0 1.00
app/serializers/translation_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/user_serializer.rb 100.00 % 15 4 4 0 23.00
app/serializers/version_serializer.rb 63.64 % 54 22 14 8 0.64
app/serializers/webhook/discord/event_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/webhook/markdown/event_serializer.rb 82.35 % 38 17 14 3 9.53
app/serializers/webhook/microsoft/event_serializer.rb 0.00 % 49 33 0 33 0.00
app/serializers/webhook/slack/event_serializer.rb 0.00 % 18 16 0 16 0.00
app/services/announcement_service.rb 45.00 % 30 20 9 11 36.60
app/services/chatbot_service.rb 57.78 % 84 45 26 19 131.60
app/services/cleanup_service.rb 0.00 % 35 23 0 23 0.00
app/services/comment_service.rb 85.71 % 56 35 30 5 33.77
app/services/contact_message_service.rb 0.00 % 19 19 0 19 0.00
app/services/demo_service.rb 0.00 % 51 42 0 42 0.00
app/services/discussion_reader_service.rb 66.67 % 12 6 4 2 0.83
app/services/discussion_service.rb 91.27 % 266 126 115 11 55.47
app/services/discussion_template_service.rb 0.00 % 72 57 0 57 0.00
app/services/document_service.rb 0.00 % 31 22 0 22 0.00
app/services/event_service.rb 73.91 % 112 46 34 12 17.91
app/services/group_export_service.rb 27.62 % 329 105 29 76 0.63
app/services/group_service.rb 97.67 % 182 86 84 2 132.49
app/services/group_service/privacy_change.rb 100.00 % 39 24 24 0 3.63
app/services/link_preview_service.rb 0.00 % 49 39 0 39 0.00
app/services/login_token_service.rb 100.00 % 10 6 6 0 7.83
app/services/markdown_service.rb 77.08 % 90 48 37 11 450.75
app/services/membership_request_service.rb 64.29 % 21 14 9 5 0.79
app/services/membership_service.rb 86.21 % 184 87 75 12 4.17
app/services/merge_users_service.rb 72.22 % 26 18 13 5 1.39
app/services/message_channel_service.rb 91.30 % 36 23 21 2 1406.61
app/services/migrate_events_service.rb 0.00 % 79 68 0 68 0.00
app/services/migrate_guests_service.rb 0.00 % 9 9 0 9 0.00
app/services/mybb_service.rb 0.00 % 75 64 0 64 0.00
app/services/newsletter_service.rb 54.55 % 61 22 12 10 1.00
app/services/notification_service.rb 88.00 % 54 25 22 3 52.88
app/services/outcome_service.rb 100.00 % 89 30 30 0 10.30
app/services/poll_service.rb 97.16 % 353 141 137 4 352.91
app/services/poll_template_service.rb 0.00 % 75 62 0 62 0.00
app/services/reaction_service.rb 100.00 % 22 13 13 0 2.77
app/services/received_email_service.rb 88.31 % 183 77 68 9 8.29
app/services/record_cache.rb 96.53 % 342 202 195 7 2192.64
app/services/record_cloner.rb 72.87 % 451 188 137 51 16.96
app/services/report_service.rb 0.00 % 460 413 0 413 0.00
app/services/retry_on_error.rb 100.00 % 11 5 5 0 191.80
app/services/search_service.rb 78.95 % 60 19 15 4 18.11
app/services/sequence_service.rb 100.00 % 25 9 9 0 503.67
app/services/stance_service.rb 82.61 % 76 46 38 8 20.65
app/services/tag_service.rb 95.16 % 111 62 59 3 1352.98
app/services/task_service.rb 96.61 % 132 59 57 2 16838.25
app/services/throttle_service.rb 100.00 % 25 14 14 0 527.57
app/services/transcription_service.rb 66.67 % 16 6 4 2 4.00
app/services/translation_service.rb 20.41 % 93 49 10 39 1173.67
app/services/user_service.rb 94.64 % 85 56 53 3 4.93
app/validators/email_validator.rb 0.00 % 7 7 0 7 0.00
app/workers/accept_membership_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/add_group_id_to_documents_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/add_heading_ids_worker.rb 0.00 % 20 19 0 19 0.00
app/workers/announce_discussion_worker.rb 0.00 % 11 10 0 10 0.00
app/workers/append_transcript_worker.rb 0.00 % 15 13 0 13 0.00
app/workers/attach_document_worker.rb 0.00 % 15 14 0 14 0.00
app/workers/close_expired_poll_worker.rb 100.00 % 10 7 7 0 9.00
app/workers/convert_discussion_templates_worker.rb 0.00 % 41 35 0 35 0.00
app/workers/convert_poll_stances_in_discussion_worker.rb 0.00 % 17 16 0 16 0.00
app/workers/deactivate_user_worker.rb 100.00 % 19 11 11 0 2.45
app/workers/destroy_discussion_worker.rb 0.00 % 7 6 0 6 0.00
app/workers/destroy_group_worker.rb 100.00 % 7 4 4 0 1.00
app/workers/destroy_record_worker.rb 100.00 % 6 4 4 0 1.00
app/workers/destroy_tag_worker.rb 0.00 % 22 17 0 17 0.00
app/workers/destroy_user_worker.rb 100.00 % 26 5 5 0 2.60
app/workers/download_attachment_worker.rb 0.00 % 7 6 0 6 0.00
app/workers/fix_stances_missing_from_threads_worker.rb 0.00 % 13 13 0 13 0.00
app/workers/generic_worker.rb 100.00 % 7 4 4 0 3053.50
app/workers/geo_location_worker.rb 0.00 % 21 17 0 17 0.00
app/workers/group_export_csv_worker.rb 0.00 % 15 14 0 14 0.00
app/workers/group_export_worker.rb 100.00 % 14 11 11 0 1.00
app/workers/migrate_guest_on_discussion_readers_and_stances.rb 0.00 % 21 18 0 18 0.00
app/workers/migrate_poll_templates_worker.rb 0.00 % 57 51 0 51 0.00
app/workers/migrate_tags_worker.rb 0.00 % 27 24 0 24 0.00
app/workers/migrate_user_worker.rb 100.00 % 85 35 35 0 4.80
app/workers/move_comments_worker.rb 100.00 % 38 21 21 0 4.43
app/workers/publish_event_worker.rb 100.00 % 7 4 4 0 249.25
app/workers/redact_user_worker.rb 100.00 % 58 19 19 0 5.21
app/workers/remove_poll_expired_from_threads_worker.rb 0.00 % 12 11 0 11 0.00
app/workers/repair_thread_worker.rb 100.00 % 8 5 5 0 1.60
app/workers/reset_poll_stance_data_worker.rb 0.00 % 11 9 0 9 0.00
app/workers/revoke_memberships_of_deactivated_users_worker.rb 0.00 % 12 11 0 11 0.00
app/workers/send_daily_catch_up_email_worker.rb 93.33 % 24 15 14 1 2.53
app/workers/update_blocked_domains_worker.rb 0.00 % 17 14 0 14 0.00
app/workers/update_poll_counts_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/update_tag_worker.rb 100.00 % 31 18 18 0 5.17
lib/analyzers/transcription_analyzer.rb 0.00 % 21 17 0 17 0.00
lib/event_bus.rb 0.00 % 28 21 0 21 0.00
lib/pie_chart.rb 0.00 % 52 46 0 46 0.00
lib/slack_mrkdwn.rb 0.00 % 169 115 0 115 0.00
lib/version.rb 0.00 % 7 7 0 7 0.00

Controllers ( 34.61% covered at 5.15 hits/line )

87 files in total.
3100 relevant lines, 1073 lines covered and 2027 lines missed. ( 34.61% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/api/b2/base_controller.rb 100.00 % 27 18 18 0 10.94
app/controllers/api/b2/discussions_controller.rb 75.00 % 12 8 6 2 1.63
app/controllers/api/b2/memberships_controller.rb 85.19 % 58 27 23 4 4.48
app/controllers/api/b2/polls_controller.rb 80.00 % 16 10 8 2 2.40
app/controllers/api/b3/users_controller.rb 100.00 % 22 15 15 0 1.93
app/controllers/api/v1/announcements_controller.rb 68.92 % 187 74 51 23 11.22
app/controllers/api/v1/attachments_controller.rb 91.67 % 37 24 22 2 0.96
app/controllers/api/v1/boot_controller.rb 0.00 % 39 35 0 35 0.00
app/controllers/api/v1/chatbots_controller.rb 0.00 % 16 14 0 14 0.00
app/controllers/api/v1/comments_controller.rb 46.67 % 21 15 7 8 0.60
app/controllers/api/v1/contact_messages_controller.rb 0.00 % 2 2 0 2 0.00
app/controllers/api/v1/demos_controller.rb 0.00 % 19 16 0 16 0.00
app/controllers/api/v1/discussion_readers_controller.rb 0.00 % 59 51 0 51 0.00
app/controllers/api/v1/discussion_templates_controller.rb 0.00 % 142 118 0 118 0.00
app/controllers/api/v1/discussions_controller.rb 75.73 % 187 103 78 25 2.69
app/controllers/api/v1/documents_controller.rb 82.61 % 56 23 19 4 2.43
app/controllers/api/v1/events_controller.rb 65.22 % 115 69 45 24 6.30
app/controllers/api/v1/group_surveys_controller.rb 0.00 % 7 6 0 6 0.00
app/controllers/api/v1/groups_controller.rb 60.00 % 73 45 27 18 0.96
app/controllers/api/v1/identities_controller.rb 0.00 % 26 21 0 21 0.00
app/controllers/api/v1/link_previews_controller.rb 0.00 % 17 14 0 14 0.00
app/controllers/api/v1/login_tokens_controller.rb 100.00 % 12 8 8 0 2.25
app/controllers/api/v1/membership_requests_controller.rb 86.36 % 37 22 19 3 1.23
app/controllers/api/v1/memberships_controller.rb 78.69 % 103 61 48 13 1.90
app/controllers/api/v1/notifications_controller.rb 0.00 % 17 15 0 15 0.00
app/controllers/api/v1/outcomes_controller.rb 100.00 % 9 5 5 0 5.60
app/controllers/api/v1/poll_templates_controller.rb 0.00 % 105 91 0 91 0.00
app/controllers/api/v1/polls_controller.rb 78.38 % 60 37 29 8 1.97
app/controllers/api/v1/profile_controller.rb 70.21 % 174 94 66 28 1.87
app/controllers/api/v1/reactions_controller.rb 86.36 % 37 22 19 3 2.00
app/controllers/api/v1/received_emails_controller.rb 86.67 % 57 30 26 4 1.10
app/controllers/api/v1/registrations_controller.rb 96.67 % 52 30 29 1 4.43
app/controllers/api/v1/reports_controller.rb 0.00 % 56 51 0 51 0.00
app/controllers/api/v1/restful_controller.rb 94.12 % 22 17 16 1 1.18
app/controllers/api/v1/search_controller.rb 85.96 % 119 57 49 8 8.49
app/controllers/api/v1/sessions_controller.rb 65.79 % 63 38 25 13 2.53
app/controllers/api/v1/snorlax_base.rb 91.67 % 289 156 143 13 76.98
app/controllers/api/v1/stances_controller.rb 86.67 % 138 75 65 10 2.63
app/controllers/api/v1/tags_controller.rb 55.00 % 36 20 11 9 0.55
app/controllers/api/v1/tasks_controller.rb 100.00 % 48 22 22 0 1.41
app/controllers/api/v1/translations_controller.rb 0.00 % 6 6 0 6 0.00
app/controllers/api/v1/trials_controller.rb 100.00 % 34 22 22 0 1.00
app/controllers/api/v1/versions_controller.rb 100.00 % 29 13 13 0 1.23
app/controllers/application_controller.rb 0.00 % 184 152 0 152 0.00
app/controllers/authenticate_by_unsubscribe_token_controller.rb 87.50 % 15 8 7 1 3.13
app/controllers/dev/base_controller.rb 0.00 % 31 26 0 26 0.00
app/controllers/dev/discussions_controller.rb 0.00 % 41 36 0 36 0.00
app/controllers/dev/nightwatch_controller.rb 0.00 % 38 34 0 34 0.00
app/controllers/dev/polls_controller.rb 0.00 % 136 103 0 103 0.00
app/controllers/dev/scenarios/auth.rb 0.00 % 163 135 0 135 0.00
app/controllers/dev/scenarios/dashboard.rb 0.00 % 23 20 0 20 0.00
app/controllers/dev/scenarios/discussion.rb 0.00 % 216 187 0 187 0.00
app/controllers/dev/scenarios/email_settings.rb 0.00 % 12 11 0 11 0.00
app/controllers/dev/scenarios/group.rb 0.00 % 226 203 0 203 0.00
app/controllers/dev/scenarios/inbox.rb 0.00 % 8 8 0 8 0.00
app/controllers/dev/scenarios/join_group.rb 0.00 % 18 17 0 17 0.00
app/controllers/dev/scenarios/membership.rb 0.00 % 13 12 0 12 0.00
app/controllers/dev/scenarios/membership_request.rb 0.00 % 11 11 0 11 0.00
app/controllers/dev/scenarios/notification.rb 0.00 % 14 13 0 13 0.00
app/controllers/dev/scenarios/profile.rb 0.00 % 29 26 0 26 0.00
app/controllers/dev/scenarios/tags.rb 0.00 % 34 31 0 31 0.00
app/controllers/dev/scenarios/util.rb 0.00 % 32 29 0 29 0.00
app/controllers/direct_uploads_controller.rb 0.00 % 27 24 0 24 0.00
app/controllers/discussions_controller.rb 100.00 % 2 1 1 0 1.00
app/controllers/email_actions_controller.rb 87.50 % 64 32 28 4 1.56
app/controllers/groups_controller.rb 82.35 % 26 17 14 3 0.94
app/controllers/help_controller.rb 0.00 % 20 17 0 17 0.00
app/controllers/identities/base_controller.rb 0.00 % 94 74 0 74 0.00
app/controllers/identities/facebook_controller.rb 0.00 % 35 27 0 27 0.00
app/controllers/identities/google_controller.rb 0.00 % 16 12 0 12 0.00
app/controllers/identities/nextcloud_controller.rb 0.00 % 20 15 0 15 0.00
app/controllers/identities/oauth_controller.rb 0.00 % 16 12 0 12 0.00
app/controllers/identities/saml_controller.rb 0.00 % 64 36 0 36 0.00
app/controllers/login_tokens_controller.rb 100.00 % 13 7 7 0 1.71
app/controllers/manifest_controller.rb 100.00 % 28 9 9 0 2.11
app/controllers/memberships_controller.rb 100.00 % 33 19 19 0 3.47
app/controllers/merge_users_controller.rb 64.71 % 27 17 11 6 0.88
app/controllers/pie_chart_controller.rb 0.00 % 25 18 0 18 0.00
app/controllers/poll_templates_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/polls_controller.rb 78.95 % 33 19 15 4 1.21
app/controllers/received_emails_controller.rb 94.12 % 38 17 16 1 8.71
app/controllers/redirect_controller.rb 83.33 % 23 12 10 2 1.00
app/controllers/robots_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/root_controller.rb 0.00 % 5 5 0 5 0.00
app/controllers/thread_templates_controller.rb 0.00 % 11 10 0 10 0.00
app/controllers/users/passwords_controller.rb 0.00 % 21 16 0 16 0.00
app/controllers/users_controller.rb 100.00 % 3 2 2 0 1.00

Channels ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Models ( 36.46% covered at 241.61 hits/line )

154 files in total.
4440 relevant lines, 1619 lines covered and 2821 lines missed. ( 36.46% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/models/ability/attachment.rb 85.71 % 13 7 6 1 734.71
app/models/ability/base.rb 94.59 % 53 37 35 2 94.89
app/models/ability/chatbot.rb 80.00 % 9 5 4 1 685.60
app/models/ability/comment.rb 100.00 % 41 18 18 0 585.50
app/models/ability/discussion.rb 91.67 % 71 36 33 3 698.44
app/models/ability/discussion_reader.rb 63.64 % 21 11 7 4 778.82
app/models/ability/discussion_template.rb 80.00 % 9 5 4 1 685.60
app/models/ability/document.rb 55.56 % 17 9 5 4 571.22
app/models/ability/event.rb 100.00 % 11 6 6 0 571.67
app/models/ability/group.rb 95.45 % 99 44 42 2 555.95
app/models/ability/identity.rb 80.00 % 9 5 4 1 685.60
app/models/ability/membership.rb 94.12 % 31 17 16 1 608.00
app/models/ability/membership_request.rb 81.82 % 19 11 9 2 626.18
app/models/ability/outcome.rb 100.00 % 26 14 14 0 763.79
app/models/ability/poll.rb 100.00 % 82 41 41 0 628.73
app/models/ability/poll_template.rb 80.00 % 9 5 4 1 685.60
app/models/ability/reaction.rb 88.89 % 17 9 8 1 762.33
app/models/ability/stance.rb 86.67 % 32 15 13 2 803.80
app/models/ability/tag.rb 87.50 % 15 8 7 1 643.50
app/models/ability/task.rb 100.00 % 9 5 5 0 686.60
app/models/ability/user.rb 100.00 % 27 13 13 0 792.46
app/models/anonymous_user.rb 72.73 % 21 11 8 3 40.00
app/models/application_record.rb 0.00 % 3 3 0 3 0.00
app/models/attachment.rb 100.00 % 2 1 1 0 1.00
app/models/blocked_domain.rb 0.00 % 2 2 0 2 0.00
app/models/boot/site.rb 0.00 % 39 38 0 38 0.00
app/models/boot/user.rb 100.00 % 30 14 14 0 3.50
app/models/calendar_invite.rb 91.30 % 35 23 21 2 5.17
app/models/chatbot.rb 88.89 % 18 9 8 1 0.89
app/models/comment.rb 93.42 % 159 76 71 5 99.66
app/models/concerns/avatar_initials.rb 0.00 % 23 15 0 15 0.00
app/models/concerns/discussion_export_relations.rb 95.00 % 32 20 19 1 0.95
app/models/concerns/events/live_update.rb 100.00 % 19 11 11 0 515.64
app/models/concerns/events/notify/author.rb 90.91 % 20 11 10 1 18.09
app/models/concerns/events/notify/by_email.rb 88.89 % 17 9 8 1 421.22
app/models/concerns/events/notify/chatbots.rb 100.00 % 6 4 4 0 451.00
app/models/concerns/events/notify/in_app.rb 100.00 % 64 27 27 0 631.89
app/models/concerns/events/notify/mentions.rb 92.86 % 26 14 13 1 224.43
app/models/concerns/group_export_relations.rb 0.00 % 145 102 0 102 0.00
app/models/concerns/group_privacy.rb 0.00 % 157 132 0 132 0.00
app/models/concerns/has_avatar.rb 0.00 % 85 75 0 75 0.00
app/models/concerns/has_created_event.rb 0.00 % 13 11 0 11 0.00
app/models/concerns/has_custom_fields.rb 0.00 % 8 8 0 8 0.00
app/models/concerns/has_defaults.rb 0.00 % 7 7 0 7 0.00
app/models/concerns/has_events.rb 0.00 % 9 8 0 8 0.00
app/models/concerns/has_experiences.rb 0.00 % 6 6 0 6 0.00
app/models/concerns/has_mentions.rb 0.00 % 59 46 0 46 0.00
app/models/concerns/has_rich_text.rb 0.00 % 139 118 0 118 0.00
app/models/concerns/has_tags.rb 0.00 % 16 13 0 13 0.00
app/models/concerns/has_timeframe.rb 0.00 % 15 11 0 11 0.00
app/models/concerns/has_tokens.rb 0.00 % 7 7 0 7 0.00
app/models/concerns/has_volume.rb 0.00 % 41 34 0 34 0.00
app/models/concerns/identities/with_client.rb 0.00 % 33 25 0 25 0.00
app/models/concerns/message_channel.rb 0.00 % 7 6 0 6 0.00
app/models/concerns/no_forbidden_emails.rb 0.00 % 8 7 0 7 0.00
app/models/concerns/no_spam.rb 0.00 % 11 8 0 8 0.00
app/models/concerns/null/group.rb 72.73 % 184 44 32 12 381.80
app/models/concerns/null/object.rb 88.24 % 63 34 30 4 14173.15
app/models/concerns/null/user.rb 85.19 % 74 27 23 4 734.81
app/models/concerns/reactable.rb 0.00 % 6 6 0 6 0.00
app/models/concerns/readable_unguessable_urls.rb 0.00 % 32 27 0 27 0.00
app/models/concerns/routing.rb 0.00 % 10 9 0 9 0.00
app/models/concerns/searchable.rb 0.00 % 25 21 0 21 0.00
app/models/concerns/self_referencing.rb 0.00 % 8 7 0 7 0.00
app/models/concerns/translatable.rb 0.00 % 28 22 0 22 0.00
app/models/concerns/uses_organisation_scope.rb 0.00 % 7 6 0 6 0.00
app/models/contact_message.rb 0.00 % 10 7 0 7 0.00
app/models/demo.rb 0.00 % 9 8 0 8 0.00
app/models/discussion.rb 94.24 % 263 139 131 8 838.88
app/models/discussion_reader.rb 93.42 % 133 76 71 5 250.13
app/models/discussion_template.rb 53.57 % 68 28 15 13 0.54
app/models/document.rb 88.57 % 59 35 31 4 67.29
app/models/event.rb 0.00 % 215 178 0 178 0.00
app/models/events/announcement_resend.rb 0.00 % 21 17 0 17 0.00
app/models/events/comment_edited.rb 100.00 % 8 5 5 0 1.80
app/models/events/comment_replied_to.rb 100.00 % 18 10 10 0 2.70
app/models/events/discussion_announced.rb 100.00 % 21 6 6 0 2.33
app/models/events/discussion_closed.rb 0.00 % 10 9 0 9 0.00
app/models/events/discussion_description_edited.rb 0.00 % 5 5 0 5 0.00
app/models/events/discussion_edited.rb 100.00 % 27 11 11 0 4.00
app/models/events/discussion_forked.rb 0.00 % 10 10 0 10 0.00
app/models/events/discussion_moved.rb 100.00 % 11 4 4 0 2.50
app/models/events/discussion_reopened.rb 0.00 % 9 8 0 8 0.00
app/models/events/discussion_title_edited.rb 0.00 % 5 5 0 5 0.00
app/models/events/group_identity_created.rb 0.00 % 12 11 0 11 0.00
app/models/events/invitation_accepted.rb 100.00 % 22 12 12 0 3.75
app/models/events/membership_created.rb 100.00 % 15 5 5 0 3.40
app/models/events/membership_request_approved.rb 100.00 % 20 12 12 0 1.08
app/models/events/membership_requested.rb 0.00 % 28 22 0 22 0.00
app/models/events/membership_resent.rb 78.57 % 28 14 11 3 0.79
app/models/events/new_comment.rb 100.00 % 25 12 12 0 61.75
app/models/events/new_coordinator.rb 100.00 % 13 7 7 0 1.29
app/models/events/new_discussion.rb 100.00 % 24 10 10 0 110.00
app/models/events/outcome_announced.rb 100.00 % 11 5 5 0 3.80
app/models/events/outcome_created.rb 100.00 % 20 8 8 0 3.88
app/models/events/outcome_review_due.rb 100.00 % 25 13 13 0 3.77
app/models/events/outcome_updated.rb 100.00 % 19 8 8 0 1.00
app/models/events/poll_announced.rb 92.31 % 37 13 12 1 1.92
app/models/events/poll_closed_by_user.rb 100.00 % 11 5 5 0 2.00
app/models/events/poll_closing_soon.rb 100.00 % 45 21 21 0 28.38
app/models/events/poll_created.rb 100.00 % 15 8 8 0 21.88
app/models/events/poll_edited.rb 100.00 % 22 8 8 0 2.50
app/models/events/poll_expired.rb 100.00 % 23 12 12 0 8.08
app/models/events/poll_option_added.rb 0.00 % 21 17 0 17 0.00
app/models/events/poll_reminder.rb 0.00 % 21 20 0 20 0.00
app/models/events/poll_reopened.rb 100.00 % 10 5 5 0 1.00
app/models/events/reaction_created.rb 100.00 % 29 14 14 0 2.79
app/models/events/stance_created.rb 86.36 % 43 22 19 3 17.86
app/models/events/stance_updated.rb 0.00 % 2 2 0 2 0.00
app/models/events/unknown_sender.rb 93.33 % 32 15 14 1 1.33
app/models/events/user_added_to_group.rb 72.73 % 19 11 8 3 0.73
app/models/events/user_joined_group.rb 0.00 % 5 5 0 5 0.00
app/models/events/user_mentioned.rb 100.00 % 19 10 10 0 11.80
app/models/events/user_reactivated.rb 0.00 % 12 9 0 9 0.00
app/models/formal_group.rb 0.00 % 2 2 0 2 0.00
app/models/forward_email_rule.rb 0.00 % 5 5 0 5 0.00
app/models/global_message_channel.rb 75.00 % 7 4 3 1 0.75
app/models/group.rb 0.00 % 476 399 0 399 0.00
app/models/group_identity.rb 92.31 % 21 13 12 1 0.92
app/models/group_survey.rb 0.00 % 5 4 0 4 0.00
app/models/guest_group.rb 0.00 % 2 2 0 2 0.00
app/models/identities/base.rb 0.00 % 37 30 0 30 0.00
app/models/identities/facebook.rb 0.00 % 32 26 0 26 0.00
app/models/identities/google.rb 0.00 % 11 10 0 10 0.00
app/models/identities/nextcloud.rb 0.00 % 11 10 0 10 0.00
app/models/identities/oauth.rb 0.00 % 10 9 0 9 0.00
app/models/identities/saml.rb 0.00 % 29 26 0 26 0.00
app/models/logged_out_user.rb 84.09 % 83 44 37 7 977.34
app/models/login_token.rb 100.00 % 31 18 18 0 11.67
app/models/member_email_alias.rb 100.00 % 8 6 6 0 3.00
app/models/membership.rb 0.00 % 95 75 0 75 0.00
app/models/membership_request.rb 94.83 % 98 58 55 3 7.64
app/models/notification.rb 100.00 % 15 11 11 0 130.55
app/models/null_discussion.rb 0.00 % 50 42 0 42 0.00
app/models/null_group.rb 100.00 % 7 4 4 0 699.50
app/models/null_poll.rb 0.00 % 54 46 0 46 0.00
app/models/outcome.rb 92.59 % 122 54 50 4 48.19
app/models/permitted_params.rb 79.17 % 254 48 38 10 11.56
app/models/poll.rb 0.00 % 524 440 0 440 0.00
app/models/poll_option.rb 100.00 % 62 24 24 0 2417.58
app/models/poll_template.rb 58.06 % 88 31 18 13 0.58
app/models/reaction.rb 78.57 % 24 14 11 3 0.86
app/models/received_email.rb 96.15 % 114 52 50 2 43.42
app/models/search_result.rb 75.00 % 31 8 6 2 0.75
app/models/site_settings.rb 0.00 % 11 11 0 11 0.00
app/models/stance.rb 89.04 % 275 146 130 16 398.42
app/models/stance_choice.rb 78.26 % 38 23 18 5 520.78
app/models/subscription.rb 0.00 % 76 63 0 63 0.00
app/models/tag.rb 0.00 % 34 13 0 13 0.00
app/models/tagging.rb 0.00 % 8 6 0 6 0.00
app/models/task.rb 100.00 % 12 8 8 0 1.13
app/models/tasks_user.rb 100.00 % 4 3 3 0 1.00
app/models/translation.rb 0.00 % 12 8 0 8 0.00
app/models/user.rb 0.00 % 391 311 0 311 0.00

Mailers ( 85.91% covered at 267.62 hits/line )

7 files in total.
149 relevant lines, 128 lines covered and 21 lines missed. ( 85.91% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/mailers/base_mailer.rb 96.15 % 42 26 25 1 351.77
app/mailers/contact_mailer.rb 0.00 % 13 12 0 12 0.00
app/mailers/event_mailer.rb 97.62 % 113 42 41 1 727.95
app/mailers/forward_mailer.rb 100.00 % 15 6 6 0 1.00
app/mailers/group_mailer.rb 100.00 % 12 6 6 0 1.67
app/mailers/task_mailer.rb 100.00 % 12 5 5 0 1.00
app/mailers/user_mailer.rb 86.54 % 120 52 45 7 2.60

Helpers ( 0.66% covered at 0.01 hits/line )

15 files in total.
1526 relevant lines, 10 lines covered and 1516 lines missed. ( 0.66% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/helpers/application_helper.rb 0.00 % 36 30 0 30 0.00
app/helpers/current_user_helper.rb 0.00 % 36 28 0 28 0.00
app/helpers/dev/dashboard_helper.rb 0.00 % 41 32 0 32 0.00
app/helpers/dev/fake_data_helper.rb 0.00 % 467 387 0 387 0.00
app/helpers/dev/ninties_movies_helper.rb 0.00 % 396 318 0 318 0.00
app/helpers/dev/scenarios_helper.rb 0.00 % 334 279 0 279 0.00
app/helpers/email_helper.rb 0.00 % 158 133 0 133 0.00
app/helpers/formatted_date_helper.rb 0.00 % 57 50 0 50 0.00
app/helpers/load_and_authorize.rb 0.00 % 7 7 0 7 0.00
app/helpers/locales_helper.rb 0.00 % 84 67 0 67 0.00
app/helpers/pending_actions_helper.rb 0.00 % 113 93 0 93 0.00
app/helpers/pretty_url_helper.rb 0.00 % 68 59 0 59 0.00
app/helpers/protected_from_forgery.rb 0.00 % 24 19 0 19 0.00
app/helpers/sentry_helper.rb 0.00 % 9 8 0 8 0.00
app/helpers/uses_metadata.rb 62.50 % 29 16 10 6 0.75

Jobs ( 32.08% covered at 27.39 hits/line )

36 files in total.
505 relevant lines, 162 lines covered and 343 lines missed. ( 32.08% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/workers/accept_membership_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/add_group_id_to_documents_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/add_heading_ids_worker.rb 0.00 % 20 19 0 19 0.00
app/workers/announce_discussion_worker.rb 0.00 % 11 10 0 10 0.00
app/workers/append_transcript_worker.rb 0.00 % 15 13 0 13 0.00
app/workers/attach_document_worker.rb 0.00 % 15 14 0 14 0.00
app/workers/close_expired_poll_worker.rb 100.00 % 10 7 7 0 9.00
app/workers/convert_discussion_templates_worker.rb 0.00 % 41 35 0 35 0.00
app/workers/convert_poll_stances_in_discussion_worker.rb 0.00 % 17 16 0 16 0.00
app/workers/deactivate_user_worker.rb 100.00 % 19 11 11 0 2.45
app/workers/destroy_discussion_worker.rb 0.00 % 7 6 0 6 0.00
app/workers/destroy_group_worker.rb 100.00 % 7 4 4 0 1.00
app/workers/destroy_record_worker.rb 100.00 % 6 4 4 0 1.00
app/workers/destroy_tag_worker.rb 0.00 % 22 17 0 17 0.00
app/workers/destroy_user_worker.rb 100.00 % 26 5 5 0 2.60
app/workers/download_attachment_worker.rb 0.00 % 7 6 0 6 0.00
app/workers/fix_stances_missing_from_threads_worker.rb 0.00 % 13 13 0 13 0.00
app/workers/generic_worker.rb 100.00 % 7 4 4 0 3053.50
app/workers/geo_location_worker.rb 0.00 % 21 17 0 17 0.00
app/workers/group_export_csv_worker.rb 0.00 % 15 14 0 14 0.00
app/workers/group_export_worker.rb 100.00 % 14 11 11 0 1.00
app/workers/migrate_guest_on_discussion_readers_and_stances.rb 0.00 % 21 18 0 18 0.00
app/workers/migrate_poll_templates_worker.rb 0.00 % 57 51 0 51 0.00
app/workers/migrate_tags_worker.rb 0.00 % 27 24 0 24 0.00
app/workers/migrate_user_worker.rb 100.00 % 85 35 35 0 4.80
app/workers/move_comments_worker.rb 100.00 % 38 21 21 0 4.43
app/workers/publish_event_worker.rb 100.00 % 7 4 4 0 249.25
app/workers/redact_user_worker.rb 100.00 % 58 19 19 0 5.21
app/workers/remove_poll_expired_from_threads_worker.rb 0.00 % 12 11 0 11 0.00
app/workers/repair_thread_worker.rb 100.00 % 8 5 5 0 1.60
app/workers/reset_poll_stance_data_worker.rb 0.00 % 11 9 0 9 0.00
app/workers/revoke_memberships_of_deactivated_users_worker.rb 0.00 % 12 11 0 11 0.00
app/workers/send_daily_catch_up_email_worker.rb 93.33 % 24 15 14 1 2.53
app/workers/update_blocked_domains_worker.rb 0.00 % 17 14 0 14 0.00
app/workers/update_poll_counts_worker.rb 0.00 % 9 8 0 8 0.00
app/workers/update_tag_worker.rb 100.00 % 31 18 18 0 5.17

Libraries ( 0.0% covered at 0.0 hits/line )

5 files in total.
206 relevant lines, 0 lines covered and 206 lines missed. ( 0.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/analyzers/transcription_analyzer.rb 0.00 % 21 17 0 17 0.00
lib/event_bus.rb 0.00 % 28 21 0 21 0.00
lib/pie_chart.rb 0.00 % 52 46 0 46 0.00
lib/slack_mrkdwn.rb 0.00 % 169 115 0 115 0.00
lib/version.rb 0.00 % 7 7 0 7 0.00

Ungrouped ( 46.66% covered at 560.22 hits/line )

144 files in total.
4818 relevant lines, 2248 lines covered and 2570 lines missed. ( 46.66% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/admin/demos.rb 0.00 % 27 23 0 23 0.00
app/admin/forward_email_rules.rb 0.00 % 20 17 0 17 0.00
app/admin/groups.rb 0.00 % 281 243 0 243 0.00
app/admin/subscriptions.rb 0.00 % 101 93 0 93 0.00
app/admin/users.rb 0.00 % 184 159 0 159 0.00
app/extras/app_config.rb 0.00 % 119 104 0 104 0.00
app/extras/clients/base.rb 77.36 % 117 53 41 12 14.94
app/extras/clients/google.rb 0.00 % 28 21 0 21 0.00
app/extras/clients/nextcloud.rb 0.00 % 36 28 0 28 0.00
app/extras/clients/oauth.rb 0.00 % 44 36 0 36 0.00
app/extras/clients/recaptcha.rb 50.00 % 17 10 5 5 0.50
app/extras/clients/request.rb 92.31 % 21 13 12 1 12.38
app/extras/clients/slack.rb 0.00 % 57 45 0 45 0.00
app/extras/clients/webhook.rb 60.00 % 23 10 6 4 4.30
app/extras/group_exporter.rb 56.00 % 57 25 14 11 1.04
app/extras/model_locator.rb 100.00 % 40 22 22 0 231.09
app/extras/poll_exporter.rb 100.00 % 68 26 26 0 1.50
app/extras/queries/admin_group_page.rb 0.00 % 56 45 0 45 0.00
app/extras/queries/explore_groups.rb 92.86 % 30 14 13 1 8.86
app/extras/queries/group_stats.rb 0.00 % 21 17 0 17 0.00
app/extras/queries/union_query.rb 100.00 % 6 4 4 0 4.50
app/extras/queries/users_by_volume_query.rb 100.00 % 34 14 14 0 408.14
app/extras/range_set.rb 100.00 % 143 68 68 0 801.37
app/extras/time_zone_to_city.rb 100.00 % 28 16 16 0 2073.50
app/extras/user_inviter.rb 94.55 % 125 55 52 3 348.69
app/extras/username_generator.rb 95.65 % 41 23 22 1 2679.13
app/queries/attachment_query.rb 100.00 % 45 10 10 0 1.80
app/queries/contactable_query.rb 100.00 % 34 15 15 0 7.73
app/queries/discussion_query.rb 86.96 % 64 23 20 3 60.57
app/queries/group_query.rb 100.00 % 14 7 7 0 2.71
app/queries/membership_query.rb 80.00 % 53 25 20 5 3.48
app/queries/poll_query.rb 82.76 % 61 29 24 5 70.38
app/queries/reaction_query.rb 100.00 % 41 19 19 0 1.63
app/queries/user_query.rb 100.00 % 72 28 28 0 1340.43
app/serializers/application_serializer.rb 91.36 % 160 81 74 7 7223.70
app/serializers/attachment_serializer.rb 100.00 % 37 19 19 0 1.32
app/serializers/author_serializer.rb 100.00 % 46 17 17 0 1166.71
app/serializers/chatbot_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/comment_serializer.rb 88.89 % 31 9 8 1 27.67
app/serializers/contact_message_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/contact_request_serializer.rb 0.00 % 2 2 0 2 0.00
app/serializers/contact_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/current_user_serializer.rb 81.82 % 24 11 9 2 3.00
app/serializers/demo_serializer.rb 0.00 % 12 10 0 10 0.00
app/serializers/discussion_reader_serializer.rb 100.00 % 28 9 9 0 3.33
app/serializers/discussion_serializer.rb 100.00 % 92 33 33 0 1454.33
app/serializers/discussion_template_serializer.rb 0.00 % 29 27 0 27 0.00
app/serializers/document_serializer.rb 100.00 % 17 9 9 0 6.78
app/serializers/event_serializer.rb 91.43 % 63 35 32 3 1563.94
app/serializers/group_serializer.rb 93.55 % 120 31 29 2 563.61
app/serializers/identity_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/locale_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/marked_as_read/discussion_serializer.rb 0.00 % 32 27 0 27 0.00
app/serializers/member_email_alias_serializer.rb 75.00 % 20 8 6 2 0.75
app/serializers/member_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/membership_request_serializer.rb 100.00 % 10 6 6 0 1.50
app/serializers/membership_serializer.rb 100.00 % 28 10 10 0 26.30
app/serializers/metadata/discussion_serializer.rb 0.00 % 11 9 0 9 0.00
app/serializers/metadata/group_serializer.rb 0.00 % 17 14 0 14 0.00
app/serializers/metadata/poll_serializer.rb 0.00 % 15 12 0 12 0.00
app/serializers/metadata/user_serializer.rb 0.00 % 15 12 0 12 0.00
app/serializers/metadata_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/model_error_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/notification/event_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/notification_serializer.rb 95.24 % 53 21 20 1 581.14
app/serializers/outcome_serializer.rb 100.00 % 28 6 6 0 6.33
app/serializers/pending/base_serializer.rb 0.00 % 57 44 0 44 0.00
app/serializers/pending/discussion_reader_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/pending/group_serializer.rb 0.00 % 39 30 0 30 0.00
app/serializers/pending/identity_serializer.rb 0.00 % 21 18 0 18 0.00
app/serializers/pending/membership_serializer.rb 0.00 % 43 32 0 32 0.00
app/serializers/pending/stance_serializer.rb 0.00 % 9 8 0 8 0.00
app/serializers/pending/token_serializer.rb 0.00 % 38 30 0 30 0.00
app/serializers/pending/user_serializer.rb 0.00 % 17 13 0 13 0.00
app/serializers/permitted_params_serializer.rb 0.00 % 12 9 0 9 0.00
app/serializers/plugin_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/poll_option_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/poll_serializer.rb 96.88 % 142 32 31 1 220.19
app/serializers/poll_template_serializer.rb 0.00 % 46 44 0 44 0.00
app/serializers/reaction_serializer.rb 100.00 % 4 3 3 0 1.00
app/serializers/received_email_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/restricted/group_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/restricted/membership_serializer.rb 0.00 % 5 5 0 5 0.00
app/serializers/restricted/user_serializer.rb 0.00 % 10 9 0 9 0.00
app/serializers/search_result_serializer.rb 100.00 % 23 4 4 0 1.00
app/serializers/search_results/base_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/search_results/comment_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/search_results/discussion_serializer.rb 0.00 % 3 3 0 3 0.00
app/serializers/stance_choice_serializer.rb 0.00 % 24 19 0 19 0.00
app/serializers/stance_serializer.rb 96.67 % 75 30 29 1 96.63
app/serializers/tag_serializer.rb 100.00 % 3 2 2 0 1.00
app/serializers/task_serializer.rb 100.00 % 14 4 4 0 1.00
app/serializers/translation_serializer.rb 0.00 % 4 4 0 4 0.00
app/serializers/user_serializer.rb 100.00 % 15 4 4 0 23.00
app/serializers/version_serializer.rb 63.64 % 54 22 14 8 0.64
app/serializers/webhook/discord/event_serializer.rb 0.00 % 7 6 0 6 0.00
app/serializers/webhook/markdown/event_serializer.rb 82.35 % 38 17 14 3 9.53
app/serializers/webhook/microsoft/event_serializer.rb 0.00 % 49 33 0 33 0.00
app/serializers/webhook/slack/event_serializer.rb 0.00 % 18 16 0 16 0.00
app/services/announcement_service.rb 45.00 % 30 20 9 11 36.60
app/services/chatbot_service.rb 57.78 % 84 45 26 19 131.60
app/services/cleanup_service.rb 0.00 % 35 23 0 23 0.00
app/services/comment_service.rb 85.71 % 56 35 30 5 33.77
app/services/contact_message_service.rb 0.00 % 19 19 0 19 0.00
app/services/demo_service.rb 0.00 % 51 42 0 42 0.00
app/services/discussion_reader_service.rb 66.67 % 12 6 4 2 0.83
app/services/discussion_service.rb 91.27 % 266 126 115 11 55.47
app/services/discussion_template_service.rb 0.00 % 72 57 0 57 0.00
app/services/document_service.rb 0.00 % 31 22 0 22 0.00
app/services/event_service.rb 73.91 % 112 46 34 12 17.91
app/services/group_export_service.rb 27.62 % 329 105 29 76 0.63
app/services/group_service.rb 97.67 % 182 86 84 2 132.49
app/services/group_service/privacy_change.rb 100.00 % 39 24 24 0 3.63
app/services/link_preview_service.rb 0.00 % 49 39 0 39 0.00
app/services/login_token_service.rb 100.00 % 10 6 6 0 7.83
app/services/markdown_service.rb 77.08 % 90 48 37 11 450.75
app/services/membership_request_service.rb 64.29 % 21 14 9 5 0.79
app/services/membership_service.rb 86.21 % 184 87 75 12 4.17
app/services/merge_users_service.rb 72.22 % 26 18 13 5 1.39
app/services/message_channel_service.rb 91.30 % 36 23 21 2 1406.61
app/services/migrate_events_service.rb 0.00 % 79 68 0 68 0.00
app/services/migrate_guests_service.rb 0.00 % 9 9 0 9 0.00
app/services/mybb_service.rb 0.00 % 75 64 0 64 0.00
app/services/newsletter_service.rb 54.55 % 61 22 12 10 1.00
app/services/notification_service.rb 88.00 % 54 25 22 3 52.88
app/services/outcome_service.rb 100.00 % 89 30 30 0 10.30
app/services/poll_service.rb 97.16 % 353 141 137 4 352.91
app/services/poll_template_service.rb 0.00 % 75 62 0 62 0.00
app/services/reaction_service.rb 100.00 % 22 13 13 0 2.77
app/services/received_email_service.rb 88.31 % 183 77 68 9 8.29
app/services/record_cache.rb 96.53 % 342 202 195 7 2192.64
app/services/record_cloner.rb 72.87 % 451 188 137 51 16.96
app/services/report_service.rb 0.00 % 460 413 0 413 0.00
app/services/retry_on_error.rb 100.00 % 11 5 5 0 191.80
app/services/search_service.rb 78.95 % 60 19 15 4 18.11
app/services/sequence_service.rb 100.00 % 25 9 9 0 503.67
app/services/stance_service.rb 82.61 % 76 46 38 8 20.65
app/services/tag_service.rb 95.16 % 111 62 59 3 1352.98
app/services/task_service.rb 96.61 % 132 59 57 2 16838.25
app/services/throttle_service.rb 100.00 % 25 14 14 0 527.57
app/services/transcription_service.rb 66.67 % 16 6 4 2 4.00
app/services/translation_service.rb 20.41 % 93 49 10 39 1173.67
app/services/user_service.rb 94.64 % 85 56 53 3 4.93
app/validators/email_validator.rb 0.00 % 7 7 0 7 0.00

app/admin/demos.rb

0.0% lines covered

23 relevant lines. 0 lines covered and 23 lines missed.
    
  1. ActiveAdmin.register Demo do
  2. includes :group
  3. includes :author
  4. filter :name
  5. controller do
  6. def permitted_params
  7. params.permit!
  8. end
  9. end
  10. form do |f|
  11. f.inputs "Details" do
  12. f.input :author_id
  13. f.input :group_id
  14. f.input :recorded_at
  15. f.input :name
  16. f.input :demo_handle
  17. f.input :priority
  18. f.input :description, as: :text
  19. end
  20. f.actions
  21. end
  22. actions :index, :show, :new, :edit, :update, :create
  23. end

app/admin/forward_email_rules.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. ActiveAdmin.register ForwardEmailRule do
  2. filter :handle
  3. filter :email
  4. controller do
  5. def permitted_params
  6. params.permit!
  7. end
  8. end
  9. form do |f|
  10. f.inputs "Details" do
  11. f.input :handle
  12. f.input :email
  13. end
  14. f.actions
  15. end
  16. actions :index, :show, :new, :edit, :update, :create
  17. end

app/admin/groups.rb

0.0% lines covered

243 relevant lines. 0 lines covered and 243 lines missed.
    
  1. ActiveAdmin.register Group, as: 'Group' do
  2. controller do
  3. def permitted_params
  4. params.permit!
  5. end
  6. def find_resource
  7. Group.friendly.find(params[:id])
  8. end
  9. end
  10. actions :index, :show, :new, :edit, :create
  11. filter :name
  12. filter :handle, as: :string
  13. filter :description
  14. filter :memberships_count
  15. filter :created_at
  16. scope :parents_only
  17. scope :not_demo
  18. member_action :update, :method => :put do
  19. group = Group.find(params[:id])
  20. group.assign_attributes_and_files(permitted_params[:group])
  21. privacy_change = GroupService::PrivacyChange.new(group)
  22. group.save!
  23. privacy_change.commit!
  24. redirect_to admin_groups_path, :notice => "Group updated"
  25. end
  26. batch_action :delete_spam do |group_ids|
  27. group_ids.each do |group_id|
  28. if Group.any_trial.exists?(group_id)
  29. group = Group.find(group_id)
  30. if user = group.creator || group.admins.first
  31. DestroyUserWorker.perform_async(user.id)
  32. end
  33. end
  34. end
  35. redirect_to admin_groups_path, notice: "#{group_ids.size} spammy groups deleted"
  36. end
  37. index :download_links => false do
  38. selectable_column
  39. column :id
  40. column :name do |g|
  41. g.full_name
  42. end
  43. column :privacy do |g|
  44. g.group_privacy
  45. end
  46. column "Members", :memberships_count
  47. column "Discussions", :discussions_count
  48. column :created_at
  49. column :description, :sortable => :description do |group|
  50. group.description
  51. end
  52. column :revoked_at
  53. column :analytics_enabled
  54. actions
  55. end
  56. show do |group|
  57. render 'graph', { group: group }
  58. render 'stats', { group: group }
  59. if defined?(SubscriptionService) && group.subscription_id
  60. render 'subscription', { subscription: Subscription.for(group)}
  61. end
  62. if group.parent_id
  63. panel("Parent group") do
  64. link_to group.parent.name, admin_group_path(group.parent)
  65. end
  66. end
  67. panel("Subgroups") do
  68. table_for group.subgroups.order(memberships_count: :desc).each do |subgroup|
  69. column :name do |g|
  70. link_to g.name, admin_group_path(g)
  71. end
  72. column :memberships_count
  73. column :discussions_count
  74. end
  75. end
  76. panel("Members") do
  77. table_for group.all_memberships.includes(:user, :inviter, :revoker).order(created_at: :desc).filter{|m| m.user }.each do |membership|
  78. column(:name) { |m| link_to m.user.name, admin_user_path(m.user) }
  79. column(:email) { |m| m.user.email }
  80. column(:admin) { |m| m.admin }
  81. column(:created_at) { |m| m.created_at }
  82. column(:accepted_at) { |m| m.accepted_at }
  83. column(:inviter) { |m| m.inviter.try(:name) }
  84. column(:revoked_at) { |m| m.revoked_at }
  85. column(:revoker) { |m| m.revoker.try(:name) }
  86. column "Toggle admin" do |m|
  87. if m.admin?
  88. link_to("remove admin", remove_admin_admin_groups_path(membership_id: m.id, group_id: m.group_id), method: :post)
  89. else
  90. link_to("add admin", add_admin_admin_groups_path(membership_id: m.id, group_id: m.group_id), method: :post)
  91. end
  92. end
  93. end
  94. end
  95. active_admin_comments
  96. attributes_table do
  97. row :id
  98. row :name
  99. row :key
  100. row :full_name
  101. row :created_at
  102. row :updated_at
  103. row :parent
  104. row :creator_id
  105. row :description
  106. row :archived_at
  107. row :discussions_count
  108. row :memberships_count
  109. row :admin_memberships_count
  110. row :unverified_memberships_count
  111. row :public_discussions_count
  112. row :payment_plan
  113. row "Group Privacy" do
  114. if group.is_visible_to_public && group.discussion_privacy_options == 'public_only'
  115. "Open"
  116. elsif group.is_visible_to_public && group.discussion_privacy_options != 'public_only'
  117. "Closed"
  118. elsif group.is_visible_to_parent_members && !group.is_visible_to_public
  119. "Closed"
  120. elsif !group.is_visible_to_parent_members && !group.is_visible_to_public
  121. "Secret"
  122. else
  123. "Group privacy unknown"
  124. end
  125. end
  126. row :is_visible_to_public
  127. row :discussion_privacy_options
  128. row :is_visible_to_parent_members
  129. row :parent_members_can_see_discussions
  130. row :members_can_add_members
  131. row :membership_granted_upon
  132. row :members_can_edit_discussions
  133. row :members_can_edit_comments
  134. row :members_can_raise_motions
  135. row :members_can_start_discussions
  136. row :members_can_create_subgroups
  137. row :handle
  138. row :is_referral
  139. row :subscription_id
  140. row :theme_id
  141. row :cover_photo_file_name
  142. row :cover_photo_content_type
  143. row :cover_photo_updated_at
  144. row :logo_file_name
  145. row :logo_content_type
  146. row :logo_file_size
  147. row :logo_updated_at
  148. end
  149. if group.archived_at.nil?
  150. panel('Archive') do
  151. link_to 'Archive this group', archive_admin_group_path(group), method: :post, data: {confirm: "Are you sure you wanna archive #{group.name}, pal?"}
  152. end
  153. else
  154. panel('Unarchive') do
  155. link_to 'Unarchive this group', unarchive_admin_group_path(group), method: :post, data: {confirm: "Are you sure you wanna unarchive #{group.name}, pal?"}
  156. end
  157. end
  158. panel 'Move group' do
  159. form action: move_admin_group_path(group), method: :post do |f|
  160. f.input type: :hidden, name: :authenticity_token
  161. f.label "Parent group id / key"
  162. f.input name: :parent_id, value: group.parent_id
  163. f.input type: :submit, value: "Move group"
  164. end
  165. end
  166. render 'delete_group', { group: group }
  167. render 'export_group', { group: group }
  168. end
  169. form do |f|
  170. f.inputs "Details" do
  171. if f.object.persisted?
  172. f.input :id, :input_html => { :disabled => true }
  173. end
  174. f.input :name, :input_html => { :disabled => f.object.persisted? }
  175. f.input :admin_tags, label: "Tags (separated by a space)"
  176. f.input :description, :input_html => { :disabled => true }
  177. f.input :parent_id, label: "Parent Id"
  178. f.input :handle, as: :string
  179. f.input :subscription_id, label: "Subscription Id"
  180. f.input :is_visible_to_public, label: "Visible to public? (will change privacy of group, subgroups, discussions)"
  181. f.input :membership_granted_upon, as: :select, collection: %w[request approval invitation]
  182. end
  183. f.actions
  184. end
  185. collection_action :import, method: :get do
  186. end
  187. collection_action :import_json, method: :post do
  188. GenericWorker.perform_async('GroupExportService', 'import', params[:url])
  189. redirect_to admin_groups_path, notice: "Import started. Check /admin/sidekiq to see when job is complete"
  190. end
  191. collection_action :add_admin, method: :post do
  192. Membership.find(params[:membership_id]).update(admin: true)
  193. redirect_to admin_group_path(Group.find(params[:group_id]))
  194. end
  195. collection_action :remove_admin, method: :post do
  196. Membership.find(params[:membership_id]).update(admin: false)
  197. redirect_to admin_group_path(Group.find(params[:group_id]))
  198. end
  199. member_action :move, method: :post do
  200. group = Group.friendly.find(params[:id])
  201. parent = Group.friendly.find(params[:parent_id])
  202. GroupService.move(group: group, parent: parent, actor: current_user)
  203. redirect_to admin_group_path(group)
  204. end
  205. member_action :handle, method: :post do
  206. params.permit(:id, :handle)
  207. group = Group.friendly.find(params[:id])
  208. group.update(handle: params[:handle])
  209. redirect_to admin_group_path(group)
  210. end
  211. member_action :archive, :method => :post do
  212. group = Group.friendly.find(params[:id])
  213. group.archive!
  214. flash[:notice] = "Archived #{group.name}"
  215. redirect_to [:admin, :groups]
  216. end
  217. member_action :unarchive, :method => :post do
  218. group = Group.friendly.find(params[:id])
  219. group.unarchive!
  220. flash[:notice] = "Unarchived #{group.name}"
  221. redirect_to [:admin, :groups]
  222. end
  223. member_action :delete_group, :method => :post do
  224. GroupService.destroy_without_warning!(params[:id])
  225. redirect_to [:admin, :groups]
  226. end
  227. member_action :export_group, method: :post do
  228. group = Group.friendly.find(params[:id])
  229. GroupExportWorker.perform_async(group.all_groups.pluck(:id), group.name, current_user.id)
  230. redirect_to admin_group_path(group)
  231. end
  232. collection_action :export_users, method: :get do
  233. render 'export_users'
  234. end
  235. collection_action :export_users_report, method: :get do
  236. @users = User.joins(:memberships).
  237. where(:'memberships.group_id' => params[:group_ids].split(' ').map(&:to_i))
  238. if params[:coordinators]
  239. @users = @users.where(:'memberships.admin' => true)
  240. end
  241. render 'export_users_report'
  242. end
  243. end

app/admin/subscriptions.rb

0.0% lines covered

93 relevant lines. 0 lines covered and 93 lines missed.
    
  1. ActiveAdmin.register Subscription do
  2. includes :groups
  3. actions :new, :create, :index, :show, :edit, :destroy
  4. filter :chargify_subscription_id
  5. filter :expires_at, as: :date_range
  6. filter :payment_method, as: :select
  7. filter :plan, as: :select
  8. filter :state, as: :select
  9. index do
  10. column :plan
  11. column 'Groups' do |subscription|
  12. if subscription.groups.any?
  13. subscription.groups.map { |group| link_to(group.name, admin_group_path(group)) }
  14. else
  15. nil
  16. end
  17. end
  18. column :state
  19. column :expires_at
  20. column :payment_method
  21. column :chargify_subscription_id
  22. column :owner
  23. actions
  24. end
  25. show do
  26. attributes_table do
  27. row :id
  28. row :plan
  29. row :state
  30. row :expires_at
  31. row :chargify_subscription_id do |subscription|
  32. if subscription.chargify_subscription_id
  33. link_to subscription.chargify_subscription_id, "http://#{ENV['CHARGIFY_APP_NAME']}.chargify.com/subscriptions/#{subscription.chargify_subscription_id}", target: '_blank'
  34. end
  35. end
  36. row :payment_method
  37. row :owner
  38. row :groups do |subscription|
  39. subscription.groups.map do |group|
  40. link_to group.name, admin_group_path(group.id)
  41. end.join(', ').html_safe
  42. end
  43. row :max_threads
  44. row :max_members
  45. row :max_orgs
  46. row :allow_guests
  47. row :allow_subgroups
  48. row :info
  49. end
  50. panel("Refresh chargify") do
  51. if subscription.chargify_subscription_id
  52. form action: refresh_admin_subscription_path(subscription), method: :post do |f|
  53. f.input type: :hidden, name: :authenticity_token
  54. f.input type: :submit, value: "refresh chargify"
  55. end
  56. else
  57. "no chargify subscription to refresh"
  58. end
  59. end
  60. end
  61. form do |f|
  62. inputs 'Subscription' do
  63. input :plan, as: :select, collection: SubscriptionService::PLANS.keys
  64. input :payment_method, as: :select, collection: Subscription::PAYMENT_METHODS
  65. input :state, as: :select, collection: ['active', 'on_hold', 'pending', 'past_due', 'canceled']
  66. input :expires_at
  67. input :max_threads
  68. input :max_members
  69. input :max_orgs
  70. input :allow_guests
  71. input :allow_subgroups
  72. input :chargify_subscription_id, label: "Chargify Subscription Id"
  73. input :owner_id, label: "Owner Id"
  74. end
  75. f.actions
  76. end
  77. member_action :update, :method => :put do
  78. subscription = Subscription.find(params[:id])
  79. subscription.update(permitted_params[:subscription])
  80. redirect_to admin_subscriptions_path, notice: "subscription updated"
  81. end
  82. member_action :refresh, :method => :post do
  83. subscription = Subscription.find(params[:id])
  84. SubscriptionService.update(subscription: subscription,
  85. params: SubscriptionService.chargify_get(subscription.chargify_subscription_id))
  86. redirect_to [:admin, subscription]
  87. end
  88. controller do
  89. def permitted_params
  90. params.permit!
  91. end
  92. end
  93. end

app/admin/users.rb

0.0% lines covered

159 relevant lines. 0 lines covered and 159 lines missed.
    
  1. ActiveAdmin.register User do
  2. actions :index, :show, :edit
  3. filter :name
  4. filter :username
  5. filter :email, as: :string
  6. filter :email_newsletter
  7. filter :email_verified
  8. filter :sign_in_count
  9. filter :detected_locale, as: :string
  10. filter :time_zone
  11. filter :created_at
  12. scope :all
  13. scope :coordinators
  14. csv do
  15. column :name
  16. column :email
  17. column :email_newsletter
  18. column :locale
  19. column :time_zone
  20. end
  21. controller do
  22. def permitted_params
  23. params.permit!
  24. end
  25. def find_resource
  26. User.friendly.find(params[:id])
  27. end
  28. end
  29. index do
  30. column :name
  31. column :email
  32. column :created_at
  33. column :last_sign_in_at
  34. column "No. of groups", :memberships_count
  35. column :deactivated_at
  36. column :email_verified
  37. column :locale
  38. column :time_zone
  39. column :bot
  40. actions
  41. end
  42. form do |f|
  43. f.inputs "Details" do
  44. f.input :name
  45. f.input :email, as: :string
  46. f.input :username, as: :string
  47. f.input :is_admin
  48. f.input :bot, label: 'Bot account (do not add to polls)'
  49. end
  50. f.actions
  51. end
  52. member_action :update, :method => :put do
  53. user = User.friendly.find(params[:id])
  54. user.name = params[:user][:name]
  55. user.email = params[:user][:email]
  56. user.username = params[:user][:username]
  57. user.is_admin = params[:user][:is_admin]
  58. user.bot = params[:user][:bot]
  59. user.save
  60. redirect_to admin_users_path, :notice => "User updated"
  61. end
  62. member_action :login_as, :method => :get do
  63. @user = User.friendly.find(params[:id])
  64. @token = @user.login_tokens.create
  65. end
  66. member_action :merge, method: :post do
  67. source = User.friendly.find(params[:id])
  68. destination = User.find_by!(email: params[:destination_email].strip)
  69. MigrateUserWorker.perform_async(source.id, destination.id)
  70. redirect_to admin_user_path(destination)
  71. end
  72. member_action :redact, method: :put do
  73. RedactUserWorker.perform_async(params[:id].to_i, current_user.id)
  74. redirect_to admin_users_path, :notice => "User scheduled for deletion immediately"
  75. end
  76. member_action :deactivate, method: :put do
  77. DeactivateUserWorker.perform_async(params[:id].to_i, current_user.id)
  78. redirect_to admin_users_path, :notice => "User scheduled for deactivation immediately"
  79. end
  80. member_action :reactivate, method: :put do
  81. GenericWorker.perform_async('UserService', 'reactivate', params[:id].to_i)
  82. redirect_to admin_users_path, :notice => "User scheduled for reactivation immediately"
  83. end
  84. member_action :delete_spam, method: :delete do
  85. DestroyUserWorker.perform_async(params[:id].to_i)
  86. redirect_to admin_users_path, :notice => "User scheduled for spam deletion immediately"
  87. end
  88. member_action :delete_identity, method: :post do
  89. User.find(params[:id]).identities.find(params[:identity_id]).destroy
  90. redirect_to admin_user_path(User.find(params[:id]))
  91. end
  92. show do |user|
  93. attributes_table do
  94. user.attributes.each do |k,v|
  95. row k.to_sym
  96. end
  97. end
  98. if !user.deactivated_at
  99. panel("Deactivate") do
  100. button_to 'Deactivate User', deactivate_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to deactivate this user? (this is reversable, user is not notified)'}
  101. end
  102. end
  103. if user.deactivated_at && user.name.present?
  104. panel("Reactivate") do
  105. button_to 'Reactivate User', reactivate_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to reactivate this user? (user is not notified)'}
  106. end
  107. end
  108. if !user.email.nil?
  109. panel("Deactivate and Redact (delete personally identifying information)") do
  110. button_to 'Redact user', redact_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to redact this user? (this is permanent, the user will be notified by email)'}
  111. end
  112. end
  113. panel("Delete spam account") do
  114. [
  115. p("Delete the user and any groups, threads, comments, votes they created. It will not groups or threads they are simply a member of."),
  116. button_to('Destroy User', delete_spam_admin_user_path(user.id), method: :delete, data: {confirm: 'Are you sure you want to destroy this user and content they authored?'})
  117. ].join.html_safe
  118. end
  119. panel("Memberships") do
  120. table_for user.all_memberships.includes(:group, :user).order(:id).each do |m|
  121. column :id
  122. column :group_name do |g|
  123. group = g.group
  124. link_to group.full_name, admin_group_path(group)
  125. end
  126. column :volume
  127. column :admin
  128. column :accepted_at
  129. column :revoked_at
  130. end
  131. end
  132. render 'notifications', { notifications: Notification.includes(:event).where(user_id: user.id).order("id DESC").limit(30) }
  133. panel("Identities") do
  134. table_for user.identities.each do |ui|
  135. column :id
  136. column :identity_type
  137. column :uid
  138. column :name
  139. column :email
  140. column :destroy do |uii|
  141. button_to 'delete', delete_identity_admin_user_path(ui.user), method: :post, params: {identity_id: uii.id}
  142. end
  143. end
  144. end
  145. panel 'Merge into another user' do
  146. form action: merge_admin_user_path(user), method: :post do |f|
  147. f.input type: :hidden, name: :authenticity_token
  148. f.label "Email address of final user account"
  149. f.input name: :destination_email
  150. f.input type: :submit, value: "Merge user"
  151. end
  152. end
  153. panel 'login as user' do
  154. a(href: login_as_admin_user_path(user), target: "_blank") do
  155. "Login as #{user.name}"
  156. end
  157. end
  158. end
  159. end

app/controllers/api/b2/base_controller.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 class API::B2::BaseController < API::V1::SnorlaxBase
  2. 1 skip_before_action :verify_authenticity_token
  3. 1 before_action :authenticate_api_key!
  4. 1 include ::LoadAndAuthorize
  5. 1 def authenticate_api_key!
  6. 25 raise CanCan::AccessDenied unless current_user
  7. end
  8. 1 def current_user
  9. 84 @current_user ||= User.active.find_by(api_key: params[:api_key])
  10. end
  11. 1 private
  12. 1 def permitted_params
  13. 10 jarams = params.dup
  14. 10 if jarams[:api_key]
  15. 10 jarams.delete(:api_key)
  16. 10 jarams.delete(:format)
  17. 10 jarams.delete(:discussion)
  18. 10 jarams.delete(:poll)
  19. 10 jarams = ActionController::Parameters.new({resource_name => jarams})
  20. end
  21. 10 @permitted_params ||= PermittedParams.new(jarams)
  22. end
  23. end

app/controllers/api/b2/discussions_controller.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. 1 class API::B2::DiscussionsController < API::B2::BaseController
  2. 1 def show
  3. self.resource = load_and_authorize(:discussion)
  4. respond_with_resource
  5. end
  6. 1 def create
  7. 4 instantiate_resource
  8. 4 DiscussionService.create(actor: current_user, discussion: @discussion, params: params)
  9. 2 respond_with_resource
  10. end
  11. end

app/controllers/api/b2/memberships_controller.rb

85.19% lines covered

27 relevant lines. 23 lines covered and 4 lines missed.
    
  1. 1 class API::B2::MembershipsController < API::B2::BaseController
  2. 1 def create
  3. 8 current_emails = User.active.where(id: group.memberships.pluck(:user_id)).pluck(:email)
  4. 4 params_emails = params.fetch(:emails, [])
  5. 4 add_emails = params_emails - current_emails
  6. 4 remove_emails = current_emails - params_emails
  7. 4 self.collection = GroupService.invite(
  8. group: group,
  9. actor: current_user,
  10. params: {recipient_emails: add_emails}
  11. )
  12. 4 PollService.group_members_added(group.id)
  13. 4 removed_user_ids = []
  14. 4 if params[:remove_absent].to_i == 1
  15. 2 Membership.where(
  16. group_id: group.id,
  17. user_id: User.where(email: remove_emails).pluck(:id)
  18. ).each do |membership|
  19. 2 removed_user_ids << membership.user_id
  20. 2 MembershipService.revoke(
  21. membership: membership,
  22. actor: current_user
  23. )
  24. end
  25. end
  26. 4 render json: {
  27. added_emails: User.where(id: collection.pluck(:user_id)).pluck(:email),
  28. removed_emails: User.where(id: removed_user_ids).pluck(:email)
  29. }
  30. end
  31. 1 def index
  32. instantiate_collection
  33. respond_with_collection
  34. end
  35. 1 def accessible_records
  36. Membership.where(group_id: group.id)
  37. end
  38. 1 def group
  39. 18 group = Group.find(params[:group_id])
  40. 17 raise ActiveRecord::RecordNotFound, "Group not found" unless group
  41. 17 unless current_user.is_admin? || current_user.adminable_groups.include?(group)
  42. 3 raise CanCan::AccessDenied, "User is not an admin"
  43. end
  44. 14 group
  45. end
  46. 1 def default_scope
  47. super.merge(include_email: true)
  48. end
  49. end

app/controllers/api/b2/polls_controller.rb

80.0% lines covered

10 relevant lines. 8 lines covered and 2 lines missed.
    
  1. 1 class API::B2::PollsController < API::B2::BaseController
  2. 1 def show
  3. self.resource = load_and_authorize(:poll)
  4. respond_with_resource
  5. end
  6. 1 def create
  7. 6 instantiate_resource
  8. 6 if PollService.create(actor: current_user, poll: @poll, params: params)
  9. 4 PollService.invite(actor: current_user, poll: @poll, params: params)
  10. 4 respond_with_resource
  11. else
  12. 1 respond_with_errors
  13. end
  14. end
  15. end

app/controllers/api/b3/users_controller.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 class API::B3::UsersController < API::V1::SnorlaxBase
  2. 1 skip_before_action :verify_authenticity_token
  3. 1 before_action :authenticate_api_key!
  4. 1 include ::LoadAndAuthorize
  5. 1 def authenticate_api_key!
  6. 7 raise CanCan::AccessDenied unless ENV.fetch('B3_API_KEY', '').length > 16
  7. 7 raise CanCan::AccessDenied unless params[:b3_api_key] == ENV['B3_API_KEY']
  8. end
  9. 1 def deactivate
  10. 2 user = User.active.find(params[:id]) # throws 404 if not present
  11. 1 DeactivateUserWorker.perform_async(user.id, user.id)
  12. 1 render json: {success: :ok}
  13. end
  14. 1 def reactivate
  15. 2 User.deactivated.find(params[:id]) # throws 404 if not present
  16. 1 UserService.reactivate(params[:id])
  17. 1 render json: {success: :ok}
  18. end
  19. end

app/controllers/api/v1/announcements_controller.rb

68.92% lines covered

74 relevant lines. 51 lines covered and 23 lines missed.
    
  1. 1 class API::V1::AnnouncementsController < API::V1::RestfulController
  2. 1 def audience
  3. current_user.ability.authorize! :show, target_model
  4. if target_model.respond_to?(:anonymous) &&
  5. target_model.anonymous &&
  6. ['decided_voters', 'undecided_voters'].include?(params[:recipient_audience])
  7. raise CanCan::AccessDenied
  8. end
  9. self.collection = AnnouncementService.audience_users(
  10. target_model,
  11. params[:recipient_audience],
  12. current_user,
  13. params[:exclude_members],
  14. params[:include_actor].present?
  15. )
  16. respond_with_collection
  17. end
  18. 1 def new_member_count
  19. current_user.ability.authorize! :show, target_model
  20. count = UserInviter.new_members_count(
  21. parent_group: target_model.parent_or_self,
  22. user_ids: String(params[:recipient_user_xids]).split('x').map(&:to_i),
  23. emails: String(params[:recipient_emails_cmr]).split(',')
  24. )
  25. render json: {count: count}
  26. end
  27. # count for number of notifications that will be send
  28. 1 def count
  29. 2 count = UserInviter.count(
  30. actor: current_user,
  31. model: target_model,
  32. emails: String(params[:recipient_emails_cmr]).split(','),
  33. user_ids: String(params[:recipient_user_xids]).split('x').map(&:to_i),
  34. chatbot_ids: String(params[:recipient_chatbot_xids]).split('x').map(&:to_i),
  35. audience: params[:recipient_audience],
  36. usernames: String(params[:recipient_usernames]).split(','),
  37. exclude_members: params[:exclude_members].present?,
  38. include_actor: params[:include_actor].present?
  39. )
  40. 2 render json: {count: count}
  41. end
  42. 1 def search
  43. # if target model has no groups, no discussions, then draw from users groups and guest threads
  44. self.collection = if params[:existing_only]
  45. target_model.members.search_for(params[:q]).limit(50)
  46. else
  47. UserQuery.invitable_search(
  48. model: target_model,
  49. actor: current_user,
  50. q: params[:q]
  51. )
  52. end
  53. respond_with_collection serializer: AuthorSerializer, root: :users
  54. end
  55. 1 def create
  56. 52 if target_model.is_a?(Group)
  57. 10 self.collection = GroupService.invite(group: target_model, actor: current_user, params: params)
  58. 8 respond_with_collection serializer: MembershipSerializer, root: :memberships
  59. 41 elsif target_model.is_a?(Discussion)
  60. 9 event = DiscussionService.invite(discussion: target_model, actor: current_user, params: params)
  61. 7 self.collection = DiscussionReader.where(discussion_id: target_model.id, user_id: event.recipient_user_ids)
  62. 7 respond_with_collection serializer: DiscussionReaderSerializer, root: :discussion_readers
  63. 32 elsif target_model.is_a?(Poll)
  64. 15 self.collection = PollService.invite(poll: target_model, actor: current_user, params: params)
  65. 13 respond_with_collection serializer: StanceSerializer, root: :stances
  66. 17 elsif target_model.is_a?(Outcome)
  67. 17 self.collection = OutcomeService.invite(outcome: target_model, actor: current_user, params: params)
  68. 15 respond_with_collection serializer: UserSerializer, root: :users
  69. end
  70. end
  71. 1 def users_notified_count
  72. # returns a count of users notified about this thing
  73. current_user.ability.authorize! :show, target_model
  74. count = Notification.
  75. joins(:event).
  76. where("events.id": target_event_ids).
  77. count("DISTINCT notifications.user_id")
  78. render json: {count: count}
  79. end
  80. 1 def history
  81. 1 notifications = {}
  82. 1 events = Event.where(kind: notification_kinds, id: target_event_ids).order('id desc').limit(1000)
  83. 1 allow_viewed = true
  84. 1 if target_model.respond_to?(:discussion) &&
  85. target_model.discussion.present? &&
  86. target_model.discussion.polls.kept.where(anonymous: true).any?
  87. allow_viewed = false
  88. end
  89. 1 if target_model.respond_to?(:poll) &&
  90. target_model.poll.present? &&
  91. target_model.poll.anonymous?
  92. allow_viewed = false
  93. end
  94. 1 Notification.includes(:user).where(event_id: events.pluck(:id)).order('users.name, users.email').each do |notification|
  95. 1 next unless notification.user
  96. 1 notifications[notification.event_id] = [] unless notifications.has_key?(notification.event_id)
  97. 1 notifications[notification.event_id] << {id: notification.id, to: (notification.user.name || notification.user.email), viewed: allow_viewed && notification.viewed }
  98. end
  99. 1 res = events.map do |event|
  100. 1 {event_id: event.id,
  101. created_at: event.created_at,
  102. author_name: event.user.name,
  103. kind: event.kind,
  104. notifications: notifications[event.id] || [] }
  105. 1 end.filter {|e| e[:notifications].size > 0}
  106. 1 render root: false, json: {allow_viewed: allow_viewed, data: res}
  107. end
  108. 1 private
  109. 1 def target_event_ids
  110. 1 if target_model.is_a?(Discussion)
  111. polls = Poll.where(discussion_id: target_model.id)
  112. outcomes = Outcome.where(poll_id: polls.map(&:id))
  113. comments = Comment.where(discussion_id: target_model.id)
  114. eventables = [target_model, polls, outcomes, comments].flatten.compact
  115. else
  116. 1 eventables = [target_model]
  117. end
  118. 1 event_ids = Event.where(kind: notification_kinds, eventable: eventables).pluck(:id)
  119. end
  120. 1 def notification_kinds
  121. 2 %w[announcement_created
  122. user_mentioned
  123. announcement_resend
  124. discussion_announced
  125. poll_announced
  126. outcome_announced
  127. outcome_created
  128. outcome_updated
  129. outcome_edited
  130. poll_created
  131. poll_edited
  132. poll_reminder
  133. new_discussion
  134. discussion_edited
  135. comment_replied_to
  136. poll_closing_soon]
  137. end
  138. 1 def default_scope
  139. 43 if target_model && target_model.respond_to?(:group_id)
  140. 43 is_admin = target_model.group_id ? target_model.group.admins.exists?(current_user.id) : target_model.admins.exists?(current_user.id)
  141. else
  142. is_admin = false
  143. end
  144. 43 super.merge(
  145. 43 include_email: (is_admin)
  146. )
  147. end
  148. 1 def authorize_model
  149. load_and_authorize(:group, :announce, optional: true) ||
  150. load_and_authorize(:discussion, :announce, optional: true) ||
  151. load_and_authorize(:poll, :announce, optional: true) ||
  152. load_and_authorize(:outcome, :announce, optional: false)
  153. end
  154. 1 def target_model
  155. 379 load_and_authorize(:group, :show, optional: true) ||
  156. load_and_authorize(:discussion, :show, optional: true) ||
  157. load_and_authorize(:comment, :show, optional: true) ||
  158. load_and_authorize(:poll, :show, optional: true) ||
  159. load_and_authorize(:outcome, :show, optional: true)
  160. end
  161. end

app/controllers/api/v1/attachments_controller.rb

91.67% lines covered

24 relevant lines. 22 lines covered and 2 lines missed.
    
  1. 1 class API::V1::AttachmentsController < API::V1::RestfulController
  2. 1 def index
  3. # Group.find_by(params[:group_id).id_and_subgroup_ids
  4. 1 group = current_user.groups.find_by!(id: params[:group_id])
  5. 1 group_ids = current_user.group_ids.intersection(group.id_and_subgroup_ids)
  6. 1 self.collection = AttachmentQuery.find(group_ids, params[:q], (params[:per] || 20), (params[:from] || 0))
  7. 1 self.collection_count = AttachmentQuery.find(group_ids, params[:q], 1000, 0).count
  8. 1 respond_with_collection
  9. end
  10. 1 def destroy
  11. 2 attachment = load_and_authorize :attachment, :destroy
  12. 1 record = attachment.record
  13. 1 attachment.purge_later
  14. 1 record.save!
  15. 1 serializer = "#{record.class.to_s}Serializer".constantize
  16. 1 root = record.class.to_s.pluralize.underscore
  17. 1 self.collection = [record]
  18. 1 render json: resources_to_serialize, scope: default_scope, each_serializer: serializer, root: root
  19. end
  20. 1 def serializer_root
  21. :attachments
  22. end
  23. 1 def serializer_class
  24. 1 AttachmentSerializer
  25. end
  26. 1 def accessible_records
  27. AttachmentQuery
  28. end
  29. 1 def serializer_root
  30. 1 'attachments'
  31. end
  32. end

app/controllers/api/v1/boot_controller.rb

0.0% lines covered

35 relevant lines. 0 lines covered and 35 lines missed.
    
  1. class API::V1::BootController < API::V1::RestfulController
  2. def site
  3. render json: Boot::Site.new.payload.merge(user_payload)
  4. EventBus.broadcast('boot_site', current_user)
  5. end
  6. def version
  7. render json: {
  8. version: Loomio::Version.current,
  9. release: AppConfig.release,
  10. reload: (params.fetch(:version, '0.0.0') < Loomio::Version.current) ||
  11. (ENV['LOOMIO_SYSTEM_RELOAD'] && AppConfig.release != params[:release]),
  12. notice: ENV['LOOMIO_SYSTEM_NOTICE']
  13. }
  14. end
  15. private
  16. def user_payload
  17. Boot::User.new(current_user,
  18. identity: serialized_pending_identity,
  19. flash: flash,
  20. channel_token: set_channel_token).payload
  21. end
  22. def set_channel_token
  23. token = SecureRandom.hex
  24. CACHE_REDIS_POOL.with do |client|
  25. client.set("/current_users/#{token}",
  26. {name: current_user.name,
  27. group_ids: current_user.group_ids,
  28. id: current_user.id}.to_json)
  29. end
  30. token
  31. end
  32. def current_user
  33. restricted_user || super
  34. end
  35. end

app/controllers/api/v1/chatbots_controller.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class API::V1::ChatbotsController < API::V1::RestfulController
  2. def index
  3. load_and_authorize(:group, :show_chatbots)
  4. self.collection = Chatbot.where(group_id: @group.id)
  5. respond_with_collection(scope: index_scope)
  6. end
  7. def test
  8. ChatbotService.publish_test!(params)
  9. head :ok
  10. end
  11. def index_scope
  12. default_scope.merge({ current_user_is_admin: @group.admins.exists?(current_user.id)})
  13. end
  14. end

app/controllers/api/v1/comments_controller.rb

46.67% lines covered

15 relevant lines. 7 lines covered and 8 lines missed.
    
  1. 1 class API::V1::CommentsController < API::V1::RestfulController
  2. 1 def discard
  3. 2 load_resource
  4. 2 @event = service.discard(comment: resource, actor: current_user)
  5. 1 respond_with_resource(scope: default_scope.merge(exclude_types: %w[discussion group user]))
  6. end
  7. 1 def undiscard
  8. load_resource
  9. @event = service.undiscard(comment: resource, actor: current_user)
  10. respond_with_resource(scope: {exclude_types: %w[discussion group user]})
  11. end
  12. 1 def destroy
  13. load_resource
  14. @event = @comment.created_event.parent
  15. destroy_action
  16. @event.reload
  17. render json: MessageChannelService.serialize_models(@event.children.compact, scope: default_scope)
  18. end
  19. end

app/controllers/api/v1/contact_messages_controller.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class API::V1::ContactMessagesController < API::V1::RestfulController
  2. end

app/controllers/api/v1/demos_controller.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. class API::V1::DemosController < API::V1::RestfulController
  2. before_action :require_current_user, only: [:clone]
  3. def index
  4. instantiate_collection
  5. respond_with_collection
  6. end
  7. def clone
  8. group = DemoService.take_demo(current_user)
  9. GenericWorker.perform_async('DemoService', 'refill_queue')
  10. self.collection = [group]
  11. respond_with_collection
  12. end
  13. def accessible_records
  14. Demo.all
  15. end
  16. end

app/controllers/api/v1/discussion_readers_controller.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. class API::V1::DiscussionReadersController < API::V1::RestfulController
  2. def index
  3. @discussion = load_and_authorize(:discussion)
  4. query = params[:query]
  5. instantiate_collection do |collection|
  6. collection = collection.where(discussion_id: @discussion.id)
  7. if query
  8. collection = collection.
  9. joins('LEFT OUTER JOIN users on discussion_readers.user_id = users.id').
  10. where("users.name ilike :first OR
  11. users.name ilike :last OR
  12. users.email ilike :first OR
  13. users.username ilike :first",
  14. first: "#{query}%", last: "% #{query}%")
  15. end
  16. collection
  17. end
  18. respond_with_collection
  19. end
  20. def make_admin
  21. current_user.ability.authorize! :make_admin, discussion_reader
  22. discussion_reader.update(admin: true)
  23. respond_with_resource
  24. end
  25. def remove_admin
  26. current_user.ability.authorize! :remove_admin, discussion_reader
  27. discussion_reader.update(admin: false)
  28. respond_with_resource
  29. end
  30. def resend
  31. current_user.ability.authorize! :resend, discussion_reader
  32. raise NotImplementedError.new
  33. end
  34. def revoke
  35. current_user.ability.authorize! :remove, discussion_reader
  36. discussion_reader.update(revoked_at: Time.zone.now, revoker_id: current_user.id)
  37. respond_with_resource
  38. end
  39. private
  40. def discussion_reader
  41. @discussion_reader = DiscussionReader.find(params[:id])
  42. end
  43. def default_scope
  44. discussion = (@discussion_reader || @discussion).discussion
  45. is_admin = discussion.group_id ? discussion.group.admins.exists?(current_user.id) : discussion.admins.exists?(current_user.id)
  46. super.merge({include_email: is_admin})
  47. end
  48. def accessible_records
  49. DiscussionReader.includes(:user, :discussion).where(discussion_id: @discussion.id)
  50. end
  51. end

app/controllers/api/v1/discussion_templates_controller.rb

0.0% lines covered

118 relevant lines. 0 lines covered and 118 lines missed.
    
  1. class API::V1::DiscussionTemplatesController < API::V1::RestfulController
  2. def browse_tags
  3. tag_counts = {}
  4. DiscussionTemplate.where(public: true).pluck(:tags).flatten.each do |tag|
  5. tag_counts[tag] ||= 0
  6. tag_counts[tag] += 1
  7. end
  8. render json: tag_counts.sort_by {|k,v| v}.to_h.keys.slice(0, 20), root: false
  9. end
  10. def browse
  11. if DiscussionTemplate.where(public: true).count == 0
  12. DiscussionTemplateService.create_public_templates
  13. end
  14. templates = DiscussionTemplate
  15. .joins("LEFT JOIN groups ON groups.id = discussion_templates.group_id LEFT JOIN subscriptions ON groups.subscription_id = subscriptions.id")
  16. .where("discussion_templates.public": true)
  17. .where("groups.handle = ? OR subscriptions.plan != ?", 'templates', 'trial')
  18. if params[:query].present?
  19. templates = templates.where("process_name ILIKE :q OR process_subtitle ILIKE :q OR tags @> ARRAY[:a]::varchar[]", q: "%#{params[:query]}%", a: Array(params[:query]))
  20. end
  21. templates = templates.limit(50).to_a
  22. authors = access_by_id(User.where(id: templates.map(&:author_id)))
  23. groups = access_by_id(Group.where(id: templates.map(&:group_id)))
  24. results = templates.map do |dt|
  25. author = authors[dt.author_id]
  26. group = groups[dt.group_id]
  27. {
  28. id: dt.id,
  29. process_name: dt.process_name,
  30. process_subtitle: dt.process_subtitle,
  31. author_name: author&.name,
  32. group_name: group&.name,
  33. avatar_url: (group&.logo_url || author&.avatar_url),
  34. tags: dt.tags
  35. }
  36. end
  37. render json: results, root: :results
  38. end
  39. def index
  40. group = current_user.groups.find_by(id: params[:group_id]) || NullGroup.new
  41. if group.discussion_templates.kept.count == 0
  42. group.discussion_templates = DiscussionTemplateService.initial_templates(group.category)
  43. end
  44. if params[:id]
  45. self.collection = Array(DiscussionTemplate.find_by(group_id: current_user.group_ids, id: params[:id]))
  46. else
  47. self.collection = group.discussion_templates
  48. end
  49. respond_with_collection
  50. end
  51. def show
  52. @discussion_template = DiscussionTemplate.where('group_id IN (?) OR public = true', current_user.group_ids).find(params[:id])
  53. respond_with_resource
  54. end
  55. def positions
  56. group = current_user.adminable_groups.find_by!(id: params[:group_id])
  57. params[:ids].each_with_index do |val, index|
  58. if val.is_a? Integer
  59. DiscussionTemplate.where(id: val, group_id: group.id).update_all(position: index)
  60. else
  61. group.discussion_template_positions[val] = index
  62. end
  63. end
  64. group.save!
  65. index
  66. end
  67. def discard
  68. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  69. @discussion_template = @group.discussion_templates.kept.find_by!(id: params[:id])
  70. @discussion_template.discard!
  71. index
  72. end
  73. def undiscard
  74. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  75. @discussion_template = @group.discussion_templates.discarded.find_by!(id: params[:id])
  76. @discussion_template.undiscard!
  77. index
  78. end
  79. def destroy
  80. @discussion_template = DiscussionTemplate.find(params[:id])
  81. current_user.adminable_groups.find(@discussion_template.group_id)
  82. @discussion_template.destroy!
  83. destroy_response
  84. end
  85. def hide
  86. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  87. if DiscussionTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
  88. @group = current_user.adminable_groups.find_by(id: params[:group_id])
  89. @group.hidden_discussion_templates ||= []
  90. @group.hidden_discussion_templates.push params[:key].parameterize
  91. @group.hidden_discussion_templates.uniq!
  92. @group.save!
  93. index
  94. else
  95. response_with_error(404)
  96. end
  97. end
  98. def unhide
  99. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  100. if DiscussionTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
  101. @group = current_user.adminable_groups.find_by(id: params[:group_id])
  102. @group.hidden_discussion_templates -= [params[:key].parameterize]
  103. @group.save!
  104. self.resource = @group
  105. index
  106. else
  107. response_with_error(404)
  108. end
  109. end
  110. private
  111. def access_by_id(collection, id_col = 'id')
  112. h = {}
  113. collection.each do |row|
  114. h[row.send(id_col)] = row
  115. end
  116. h
  117. end
  118. end

app/controllers/api/v1/discussions_controller.rb

75.73% lines covered

103 relevant lines. 78 lines covered and 25 lines missed.
    
  1. 1 class API::V1::DiscussionsController < API::V1::RestfulController
  2. 1 def create
  3. 16 instantiate_resource
  4. 15 if resource_params[:forked_event_ids] && resource_params[:forked_event_ids].any?
  5. EventService.move_comments(discussion: create_action.discussion, params: resource_params, actor: current_user)
  6. else
  7. 15 create_action
  8. end
  9. 11 respond_with_resource
  10. end
  11. 1 def create_action
  12. 15 @event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
  13. end
  14. 1 def show
  15. 6 load_and_authorize(:discussion)
  16. 4 if resource.closed_at && resource.closer_id.nil?
  17. if closed_event = Event.where(discussion_id: resource.id, kind: 'discussion_closed').order(:id).last
  18. resource.update_attribute(:closer_id, closed_event.user_id)
  19. else
  20. resource.update_attribute(:closer_id, resource.author_id)
  21. end
  22. end
  23. # this is desperation in code, but better than auto create when nil on method call
  24. 4 if resource.created_event.nil?
  25. EventService.repair_thread(resource.id)
  26. resource.reload
  27. end
  28. 4 accept_pending_membership
  29. 4 respond_with_resource
  30. end
  31. 1 def index
  32. 7 load_and_authorize(:group, optional: true)
  33. 7 instantiate_collection do |collection|
  34. 7 DiscussionQuery.filter(chain: collection, filter: params[:filter])
  35. end
  36. 7 respond_with_collection
  37. end
  38. 1 def dashboard
  39. 5 raise CanCan::AccessDenied.new unless current_user.is_logged_in?
  40. 4 @accessible_records = DiscussionQuery.dashboard(user: current_user)
  41. 8 instantiate_collection { |collection| collection.is_open.order_by_latest_activity }
  42. 4 respond_with_collection
  43. end
  44. 1 def direct
  45. @accessible_records = DiscussionQuery.visible_to(
  46. user: current_user,
  47. or_public: false,
  48. or_subgroups: false,
  49. group_ids: nil,
  50. only_direct: true)
  51. instantiate_collection { |collection| collection.order_by_latest_activity }
  52. respond_with_collection
  53. end
  54. 1 def inbox
  55. 5 raise CanCan::AccessDenied.new unless current_user.is_logged_in?
  56. 4 @accessible_records = DiscussionQuery.inbox(user: current_user)
  57. 8 instantiate_collection { |collection| collection.recent.order_by_latest_activity }
  58. 4 respond_with_collection
  59. end
  60. 1 def search
  61. load_and_authorize(:group)
  62. instantiate_collection { |collection| collection.search_for(params.require(:q)) }
  63. respond_with_collection
  64. end
  65. 1 def move
  66. 1 @event = service.move discussion: load_resource, params: params, actor: current_user
  67. 1 respond_with_resource
  68. end
  69. 1 def history
  70. load_and_authorize(:discussion)
  71. if @discussion.polls.kept.where(anonymous:true).any?
  72. render root: false, json: {message: I18n.t("discussion_last_seen_by.disabled_anonymous_polls")}, status: 403
  73. else
  74. res = DiscussionReader.joins(:user).where(discussion: @discussion).where.not(last_read_at: nil).map do |reader|
  75. {reader_id: reader.id,
  76. last_read_at: reader.last_read_at,
  77. user_name: reader.user.name_or_username }
  78. end
  79. render root: false, json: res
  80. end
  81. end
  82. 1 def mark_as_seen
  83. 2 service.mark_as_seen discussion: load_resource, actor: current_user
  84. 1 respond_ok
  85. end
  86. 1 def mark_as_read
  87. 1 service.mark_as_read(discussion: load_resource, params: params, actor: current_user)
  88. 1 respond_ok
  89. end
  90. 1 def dismiss
  91. 1 service.dismiss discussion: load_resource, params: params, actor: current_user
  92. 1 respond_with_resource
  93. end
  94. 1 def recall
  95. 1 service.recall discussion: load_resource, params: params, actor: current_user
  96. 1 respond_with_resource
  97. end
  98. 1 def close
  99. 3 @event = service.close discussion: load_resource, actor: current_user
  100. 1 respond_with_resource
  101. end
  102. 1 def reopen
  103. 3 @event = service.reopen discussion: load_resource, actor: current_user
  104. 1 respond_with_resource
  105. end
  106. 1 def move_comments
  107. 7 EventService.move_comments(discussion: load_resource, params: params, actor: current_user)
  108. 5 respond_with_resource
  109. end
  110. 1 def pin
  111. 3 service.pin discussion: load_resource, actor: current_user
  112. 1 respond_with_resource
  113. end
  114. 1 def unpin
  115. 1 service.unpin discussion: load_resource, actor: current_user
  116. 1 respond_with_resource
  117. end
  118. 1 def set_volume
  119. 2 update_reader volume: params[:volume]
  120. end
  121. 1 def discard
  122. @discussion = load_resource
  123. service.discard discussion: @discussion, actor: current_user
  124. respond_with_resource
  125. end
  126. 1 private
  127. 1 def group_ids
  128. 7 case params[:subgroups]
  129. when 'all'
  130. Array(@group&.id_and_subgroup_ids)
  131. when 'mine'
  132. if current_user.is_logged_in?
  133. [@group&.id].concat(current_user.group_ids & @group&.id_and_subgroup_ids)
  134. else
  135. [@group&.id]
  136. end
  137. else
  138. 7 [@group&.id]
  139. end.compact
  140. end
  141. 1 def discussion_ids
  142. 7 params.fetch(:xids, '').split('x').map(&:to_i).uniq
  143. end
  144. 1 def split_tags
  145. 7 Array(params[:tags].to_s).reject(&:blank?)
  146. end
  147. 1 def accessible_records
  148. 15 @accessible_records ||= DiscussionQuery.visible_to(
  149. user: current_user,
  150. group_ids: group_ids,
  151. tags: split_tags,
  152. discussion_ids: discussion_ids)
  153. end
  154. 1 def update_reader(params = {})
  155. 2 service.update_reader discussion: load_resource, params: params, actor: current_user
  156. 1 respond_with_resource
  157. end
  158. end

app/controllers/api/v1/documents_controller.rb

82.61% lines covered

23 relevant lines. 19 lines covered and 4 lines missed.
    
  1. 1 class API::V1::DocumentsController < API::V1::RestfulController
  2. 1 def for_group
  3. 7 self.collection = page_collection(for_group_documents).search_for(params[:q])
  4. 5 cache = RecordCache.for_collection(collection, current_user, exclude_types)
  5. 5 respond_with_collection scope: { group_id: @group.id, cache: cache }, serializer: DocumentSerializer
  6. end
  7. 1 def for_discussion
  8. 2 load_and_authorize(:discussion)
  9. 1 self.collection = Queries::UnionQuery.for(:documents, [
  10. @discussion.documents,
  11. @discussion.poll_documents,
  12. @discussion.comment_documents
  13. ])
  14. 1 respond_with_collection
  15. end
  16. 1 private
  17. 1 def for_group_documents
  18. 7 if current_user.ability.can?(:see_private_content, load_and_authorize(:group))
  19. 5 private_group_documents
  20. else
  21. public_group_documents
  22. end.order(created_at: :desc)
  23. end
  24. 1 def private_group_documents
  25. 5 group_ids = case params[:subgroups]
  26. when 'mine', 'all'
  27. @group.id_and_subgroup_ids
  28. else
  29. 5 Array(@group.id)
  30. end
  31. 5 Document.where(group_id: group_ids)
  32. end
  33. 1 def public_group_documents
  34. Queries::UnionQuery.for(:documents, [
  35. @group.documents,
  36. @group.public_discussion_documents,
  37. @group.public_comment_documents ])
  38. end
  39. 1 def accessible_records
  40. (
  41. load_and_authorize(:group, optional: true) ||
  42. load_and_authorize(:discussion, optional: true) ||
  43. load_and_authorize(:comment, optional: true) ||
  44. load_and_authorize(:poll, optional: true) ||
  45. load_and_authorize(:outcome)
  46. ).documents
  47. end
  48. end

app/controllers/api/v1/events_controller.rb

65.22% lines covered

69 relevant lines. 45 lines covered and 24 lines missed.
    
  1. 1 class API::V1::EventsController < API::V1::RestfulController
  2. 1 def position_keys
  3. load_and_authorize(:discussion)
  4. keys = Event.where(discussion_id: params[:discussion_id]).pluck(:position_key).sort
  5. render json: keys, root: 'position_keys'
  6. end
  7. 1 def timeline
  8. load_and_authorize(:discussion)
  9. data = Event.where(discussion_id: params[:discussion_id])
  10. .order(:position_key)
  11. .pluck(:position_key, :sequence_id, :created_at, :user_id, :depth, :descendant_count)
  12. render json: data.to_json, root: 'timeline'
  13. end
  14. 1 def remove_from_thread
  15. service.remove_from_thread(event: load_resource, actor: current_user)
  16. respond_with_resource
  17. end
  18. 1 def comment
  19. 2 load_and_authorize(:discussion)
  20. 2 self.resource = Event.find_by!(kind: "new_comment", eventable_type: "Comment", eventable_id: params[:comment_id])
  21. 1 respond_with_resource
  22. end
  23. 1 def pin
  24. 1 @event = Event.find(params[:id])
  25. 1 current_user.ability.authorize!(:pin, @event)
  26. 1 @event.update(pinned: true, pinned_title: params[:pinned_title])
  27. 1 render json: MessageChannelService.serialize_models(@event, scope: default_scope)
  28. end
  29. 1 def unpin
  30. @event = Event.find(params[:id])
  31. current_user.ability.authorize!(:unpin, @event)
  32. @event.update(pinned: false)
  33. render json: MessageChannelService.serialize_models(@event, scope: default_scope)
  34. end
  35. 1 private
  36. 1 def order
  37. 56 %w(sequence_id position position_key).detect {|col| col == params[:order] } || "sequence_id"
  38. end
  39. 1 def per
  40. 7 (params[:per] || default_page_size).to_i
  41. end
  42. 1 def from
  43. 7 if params[:from_sequence_id_of_position]
  44. position = [params[:from_sequence_id_of_position].to_i, 1].max
  45. Event.find_by!(discussion: @discussion, depth: 1, position: position)&.sequence_id
  46. 7 elsif params[:comment_id]
  47. Event.find_by!(kind: "new_comment", eventable_type: "Comment", eventable_id: params[:comment_id])&.sequence_id
  48. else
  49. 7 params[:from] || 0
  50. end
  51. end
  52. 1 def accessible_records
  53. 8 load_and_authorize(:discussion)
  54. 7 records = Event.where(discussion_id: @discussion.id)
  55. 7 if %w[position_key sequence_id].include?(params[:order_by])
  56. records = records.order("#{params[:order_by]}#{params[:order_desc] ? " DESC" : ''}")
  57. else
  58. 7 records = records.where("#{order} >= ?", from)
  59. end
  60. 7 if params[:unread] == 'true'
  61. reader = DiscussionReader.for(user: current_user, discussion: @discussion)
  62. # could also be where in unread_ranges, but there is a bug on http://localhost:8080/s/njwV5RpS
  63. records = records.where.not(sequence_id: reader.read_ranges.map{ |range| range[0]..range[1] })
  64. end
  65. 7 if params[:pinned] == 'true'
  66. records = records.where(pinned: true)
  67. end
  68. 7 if params[:kind]
  69. records = records.where("kind in (?)", params[:kind].split(','))
  70. end
  71. 7 %w(parent_id depth sequence_id position position_key).each do |name|
  72. 35 records = records.where(name => params[name]) if params[name]
  73. # records = records.where("#{name} >= ?", params["min_#{name}"]) if params["min_#{name}"]
  74. # records = records.where("#{name} <= ?", params["max_#{name}"]) if params["max_#{name}"]
  75. 35 records = records.where("#{name} = ?", params["#{name}"]) if params["#{name}"]
  76. 35 records = records.where("#{name} < ?", params["#{name}_lt"]) if params["#{name}_lt"]
  77. 35 records = records.where("#{name} > ?", params["#{name}_gt"]) if params["#{name}_gt"]
  78. 35 records = records.where("#{name} <= ?", params["#{name}_lte"]) if params["#{name}_lte"]
  79. 35 records = records.where("#{name} >= ?", params["#{name}_gte"]) if params["#{name}_gte"]
  80. 35 records = records.where("#{name} like ?", params["#{name}_sw"]+"%") if params["#{name}_sw"]
  81. end
  82. # records = records.where("position_key like ?", params["position_key_sw"]+"%") if params["position_key_sw"]
  83. 7 records
  84. end
  85. 1 def page_collection(collection)
  86. 7 if params[:until_sequence_id_of_position]
  87. position = [params[:until_sequence_id_of_position].to_i, @discussion.created_event.child_count].min
  88. event = Event.find_by!(discussion: @discussion, depth: 1, position: position)
  89. max_sequence_id = event.sequence_id + event.child_count
  90. collection.where("sequence_id <= ?", max_sequence_id).order('depth, position').limit(per)
  91. else
  92. 7 collection.order(order).limit(per)
  93. end
  94. end
  95. 1 def default_page_size
  96. 5 30
  97. end
  98. end

app/controllers/api/v1/group_surveys_controller.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class API::V1::GroupSurveysController < API::V1::RestfulController
  2. def create
  3. service.create(params: resource_params, actor: current_user)
  4. render json: { success: :ok }
  5. end
  6. end

app/controllers/api/v1/groups_controller.rb

60.0% lines covered

45 relevant lines. 27 lines covered and 18 lines missed.
    
  1. 1 class API::V1::GroupsController < API::V1::RestfulController
  2. 1 def token
  3. self.resource = load_and_authorize(:group, :invite_people)
  4. respond_with_resource scope: {include_token: true, exclude_types: ['membership', 'user']}
  5. end
  6. 1 def suggest_handle
  7. 3 render json: { handle: service.suggest_handle(name: params[:name], parent_handle: params[:parent_handle]) }
  8. end
  9. 1 def reset_token
  10. self.resource = load_and_authorize(:group, :invite_people)
  11. resource.update(token: resource.class.generate_unique_secure_token)
  12. respond_with_resource scope: {include_token: true, exclude_types: ['membership', 'user']}
  13. end
  14. 1 def show
  15. 6 self.resource = load_and_authorize(:group)
  16. 5 accept_pending_membership
  17. 5 respond_with_resource
  18. end
  19. 1 def index
  20. 1 ids = params.fetch(:xids, '').split('x').map(&:to_i)
  21. 1 if ids.length > 0
  22. 1 instantiate_collection do |collection|
  23. 1 collection = GroupQuery.visible_to(user: current_user, show_public: true).where(id: ids)
  24. end
  25. else
  26. order_attributes = ['created_at', 'memberships_count']
  27. order = (order_attributes.include? params[:order])? "groups.#{params[:order]} DESC" : 'groups.memberships_count DESC'
  28. instantiate_collection { |collection| collection.search_for(params[:q]).order(order) }
  29. end
  30. 1 respond_with_collection
  31. end
  32. 1 def count_explore_results
  33. 1 render json: { count: Queries::ExploreGroups.new.search_for(params[:q]).count }
  34. end
  35. 1 def subgroups
  36. self.collection = load_and_authorize(:group).subgroups.select { |g| current_user.can? :show, g }
  37. respond_with_collection
  38. end
  39. 1 def upload_photo
  40. ensure_photo_params
  41. service.update group: load_resource, actor: current_user, params: { params[:kind] => params[:file] }
  42. respond_with_resource
  43. end
  44. 1 def export #json
  45. 2 service.export(group: load_and_authorize(:group, :export), actor: current_user)
  46. 1 render json: { success: :ok }
  47. end
  48. 1 def export_csv
  49. group = load_and_authorize(:group, :export)
  50. GroupExportCsvWorker.perform_async(group.id, current_user.id)
  51. render json: { success: :ok }
  52. end
  53. 1 private
  54. 1 def ensure_photo_params
  55. params.require(:file)
  56. raise ActionController::UnpermittedParameters.new([:kind]) unless ['logo', 'cover_photo'].include? params.require(:kind)
  57. end
  58. 1 def accessible_records
  59. 1 Queries::ExploreGroups.new
  60. end
  61. end

app/controllers/api/v1/identities_controller.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. class API::V1::IdentitiesController < API::V1::RestfulController
  2. ACTION_NAMES = %w(channels admin_groups)
  3. def command
  4. current_user.ability.authorize! :show, identity
  5. if valid_command?
  6. render json: api_response.json, root: false
  7. else
  8. render json: { error: "#{params[:command]} is invalid for this identity" }, status: :bad_request
  9. end
  10. end
  11. private
  12. def valid_command?
  13. ACTION_NAMES.include?(params[:command]) && identity.respond_to?(params[:command])
  14. end
  15. def identity
  16. @identity ||= Identities::Base.find(params[:id])
  17. end
  18. def api_response
  19. @api_response ||= identity.send(params[:command])
  20. end
  21. end

app/controllers/api/v1/link_previews_controller.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class API::V1::LinkPreviewsController < API::V1::RestfulController
  2. def create
  3. # require logged in user
  4. # add rate limit of 100 per hour per user
  5. previews = LinkPreviewService.fetch_urls(filtered_urls)
  6. render json: {previews: previews}
  7. end
  8. private
  9. def filtered_urls
  10. known_urls = []
  11. if d = Discussion.find_by(id: params[:discussion_id])
  12. known_urls = DiscussionService.extract_link_preview_urls(d)
  13. end
  14. params[:urls].reject {|url| known_urls.include?(url) }
  15. end
  16. end

app/controllers/api/v1/login_tokens_controller.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class API::V1::LoginTokensController < API::V1::RestfulController
  2. 1 def create
  3. 4 save_detected_locale(login_token_user)
  4. 2 service.create(actor: login_token_user, uri: URI::parse(request.referrer.to_s))
  5. 2 render json: { success: :ok }
  6. end
  7. 1 private
  8. 1 def login_token_user
  9. 6 User.find_by!(email: params.require(:email))
  10. end
  11. end

app/controllers/api/v1/membership_requests_controller.rb

86.36% lines covered

22 relevant lines. 19 lines covered and 3 lines missed.
    
  1. 1 class API::V1::MembershipRequestsController < API::V1::RestfulController
  2. 1 before_action :authorize, only: [:pending, :previous]
  3. 1 def pending
  4. 1 @membership_requests = page_collection(@group.membership_requests.pending)
  5. 1 respond_with_collection
  6. end
  7. 1 def my_pending
  8. load_and_authorize :group
  9. @membership_requests = @group.membership_requests.pending.where(requestor_id: current_user.id)
  10. respond_with_collection
  11. end
  12. 1 def previous
  13. 1 @membership_requests = page_collection(@group.membership_requests.responded_to)
  14. 1 respond_with_collection
  15. end
  16. 1 def approve
  17. 2 service.approve(membership_request: load_resource, actor: current_user)
  18. 1 respond_with_resource
  19. end
  20. 1 def ignore
  21. 2 service.ignore(membership_request: load_resource, actor: current_user)
  22. 1 respond_with_resource
  23. end
  24. 1 private
  25. 1 def authorize
  26. 4 load_and_authorize :group
  27. 4 current_user.ability.authorize! :manage_membership_requests, @group
  28. end
  29. end

app/controllers/api/v1/memberships_controller.rb

78.69% lines covered

61 relevant lines. 48 lines covered and 13 lines missed.
    
  1. 1 class API::V1::MembershipsController < API::V1::RestfulController
  2. 1 load_resource only: [:set_volume]
  3. 1 def index
  4. 5 instantiate_collection do |collection|
  5. 5 %w[user_xids].each do |key|
  6. 5 next unless params.has_key? key
  7. params[key.gsub("_xids", "_ids")] = params[key].split('x').map(&:to_i)
  8. params.delete(key)
  9. end
  10. 5 MembershipQuery.search(chain: collection, params: params).order('memberships.group_id, memberships.admin desc, memberships.created_at desc')
  11. end
  12. 5 respond_with_collection(scope: index_scope)
  13. end
  14. 1 def destroy_response
  15. render json: Array(resource.group), each_serializer: GroupSerializer, root: :groups, scope: {}
  16. end
  17. # move to profile controller later
  18. 1 def for_user
  19. 1 load_and_authorize :user
  20. 1 same_group_ids = current_user.group_ids & @user.group_ids
  21. 1 public_group_ids = @user.groups.where(listed_in_explore: true).pluck(:id)
  22. 1 instantiate_collection do |collection|
  23. 1 Membership.joins(:group).where(group_id: same_group_ids + public_group_ids, user_id: @user.id).active.order('groups.full_name')
  24. end
  25. 1 respond_with_collection serializer: MembershipSerializer
  26. end
  27. 1 def join_group
  28. event = service.join_group group: load_and_authorize(:group), actor: current_user
  29. @membership = event.eventable
  30. respond_with_resource
  31. end
  32. 1 def my_memberships
  33. @memberships = current_user.memberships.includes(:user, :inviter)
  34. respond_with_collection
  35. end
  36. 1 def resend
  37. 3 service.resend membership: load_resource, actor: current_user
  38. 1 respond_with_resource
  39. end
  40. 1 def make_admin
  41. service.make_admin(membership: load_resource, actor: current_user)
  42. respond_with_resource
  43. end
  44. 1 def remove_admin
  45. service.remove_admin(membership: load_resource, actor: current_user)
  46. respond_with_resource
  47. end
  48. 1 def set_volume
  49. 3 service.set_volume membership: resource, params: params.slice(:volume, :apply_to_all), actor: current_user
  50. 3 respond_with_resource
  51. end
  52. 1 def save_experience
  53. 2 raise ActionController::ParameterMissing.new(:experience) unless params[:experience]
  54. 2 service.save_experience membership: load_resource, actor: current_user, params: { experience: params[:experience] }
  55. 1 respond_with_resource
  56. end
  57. 1 def user_name
  58. 6 user = User.active.find(params[:id])
  59. 6 if (user.name.blank? || !user.email_verified) &&
  60. 5 (user.group_ids & current_user.adminable_group_ids).length > 0
  61. 3 user.update(name: params[:name], username: params[:username])
  62. 3 self.resource = user
  63. 3 respond_with_resource
  64. else
  65. 3 error_response(403)
  66. end
  67. end
  68. 1 private
  69. 1 def destroy_action
  70. service.revoke(**{resource_symbol => resource, actor: current_user})
  71. end
  72. 1 def valid_orders
  73. 6 ['memberships.created_at', 'memberships.created_at desc', 'users.name', 'admin desc', 'accepted_at desc', 'accepted_at']
  74. end
  75. 1 def index_scope
  76. 5 default_scope.merge({ current_user_is_admin: model.admins.exists?(current_user.id), include_inviter: true })
  77. end
  78. 1 def model
  79. 5 load_and_authorize(:group, :see_private_content, optional: true) ||
  80. load_and_authorize(:discussion, optional: true) ||
  81. load_and_authorize(:poll, optional: true) ||
  82. NullGroup.new
  83. end
  84. 1 def accessible_records
  85. 6 MembershipQuery.visible_to(user: current_user)
  86. end
  87. end

app/controllers/api/v1/notifications_controller.rb

0.0% lines covered

15 relevant lines. 0 lines covered and 15 lines missed.
    
  1. class API::V1::NotificationsController < API::V1::RestfulController
  2. def index
  3. self.collection = accessible_records.limit(50).select do |notification|
  4. current_user.can? :show, notification.event.eventable
  5. end
  6. respond_with_collection
  7. end
  8. def viewed
  9. service.viewed(user: current_user)
  10. render json: { success: :ok }
  11. end
  12. def accessible_records
  13. current_user.notifications.includes(:actor, event: :eventable).order(id: :desc)
  14. end
  15. end

app/controllers/api/v1/outcomes_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class API::V1::OutcomesController < API::V1::RestfulController
  2. 1 def create_action
  3. 9 @event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
  4. end
  5. 1 def exclude_types
  6. 16 %w[discussion event]
  7. end
  8. end

app/controllers/api/v1/poll_templates_controller.rb

0.0% lines covered

91 relevant lines. 0 lines covered and 91 lines missed.
    
  1. class API::V1::PollTemplatesController < API::V1::RestfulController
  2. def index
  3. group = current_user.groups.find_by(id: params[:group_id]) || NullGroup.new
  4. if params[:key_or_id].present? && (params[:key_or_id].to_i.to_s == params[:key_or_id].to_s)
  5. @poll_template = PollTemplate.find_by(group_id: current_user.group_ids, id: params[:key_or_id])
  6. respond_with_resource
  7. else
  8. self.collection = PollTemplateService.group_templates(group: group)
  9. respond_with_collection
  10. end
  11. end
  12. def show
  13. @poll_template = PollTemplate.find_by(group_id: current_user.group_ids, id: params[:id])
  14. respond_with_resource
  15. end
  16. def update
  17. if params[:id].to_i.to_s != params[:id].to_s
  18. self.resource = PollTemplate.find_by(group_id: params[:poll_template][:group_id], key: params[:id])
  19. else
  20. load_resource
  21. end
  22. update_action
  23. update_response
  24. end
  25. def positions
  26. group = current_user.adminable_groups.find_by!(id: params[:group_id])
  27. params[:ids].each_with_index do |val, index|
  28. if val.is_a? Integer
  29. PollTemplate.where(id: val, group_id: group.id).update_all(position: index)
  30. else
  31. group.poll_template_positions[val] = index
  32. end
  33. end
  34. group.save!
  35. index
  36. end
  37. def settings
  38. group = current_user.adminable_groups.find_by!(id: params[:group_id])
  39. if params.has_key?(:categorize_poll_templates)
  40. group.categorize_poll_templates = params[:categorize_poll_templates]
  41. group.save!
  42. MessageChannelService.publish_models([group], group_id: group.id)
  43. success_response
  44. else
  45. error_response(404)
  46. end
  47. end
  48. def discard
  49. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  50. @poll_template = @group.poll_templates.kept.find_by!(id: params[:id])
  51. @poll_template.discard!
  52. index
  53. end
  54. def undiscard
  55. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  56. @poll_template = @group.poll_templates.discarded.find_by!(id: params[:id])
  57. @poll_template.undiscard!
  58. index
  59. end
  60. def destroy
  61. @poll_template = PollTemplate.find(params[:id])
  62. current_user.adminable_groups.find(@poll_template.group_id)
  63. @poll_template.destroy!
  64. destroy_response
  65. end
  66. def hide
  67. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  68. if PollTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
  69. @group = current_user.adminable_groups.find_by(id: params[:group_id])
  70. @group.hidden_poll_templates ||= []
  71. @group.hidden_poll_templates.push params[:key].parameterize
  72. @group.hidden_poll_templates.uniq!
  73. @group.save!
  74. index
  75. else
  76. response_with_error(404)
  77. end
  78. end
  79. def unhide
  80. @group = current_user.adminable_groups.find_by!(id: params[:group_id])
  81. if PollTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
  82. @group = current_user.adminable_groups.find_by(id: params[:group_id])
  83. @group.hidden_poll_templates -= [params[:key].parameterize]
  84. @group.save!
  85. self.resource = @group
  86. index
  87. else
  88. response_with_error(404)
  89. end
  90. end
  91. end

app/controllers/api/v1/polls_controller.rb

78.38% lines covered

37 relevant lines. 29 lines covered and 8 lines missed.
    
  1. 1 class API::V1::PollsController < API::V1::RestfulController
  2. 1 def show
  3. 2 self.resource = load_and_authorize(:poll)
  4. 1 accept_pending_membership
  5. 1 respond_with_resource
  6. end
  7. 1 def remind
  8. event = service.remind(poll: load_and_authorize(:poll), actor: current_user, params: resource_params)
  9. render json: {count: event.recipient_user_ids.count}
  10. end
  11. 1 def index
  12. 8 instantiate_collection do |collection|
  13. 8 PollQuery.filter(chain: collection, params: params).order(created_at: :desc)
  14. end
  15. 8 respond_with_collection
  16. end
  17. 1 def close
  18. 4 @event = service.close(poll: load_resource, actor: current_user)
  19. 1 respond_with_resource
  20. end
  21. 1 def reopen
  22. 3 @event = service.reopen(poll: load_resource, params: resource_params, actor: current_user)
  23. 1 respond_with_resource
  24. end
  25. 1 def discard
  26. 2 load_resource
  27. 2 @event = service.discard(poll: resource, actor: current_user)
  28. 1 respond_with_resource
  29. end
  30. 1 def add_to_thread
  31. 1 @event = service.add_to_thread(poll: load_resource, params: params, actor: current_user)
  32. 1 respond_with_resource
  33. end
  34. 1 def voters
  35. load_and_authorize(:poll)
  36. if !@poll.anonymous
  37. self.collection = User.where(id: @poll.voter_ids)
  38. else
  39. self.collection = User.none
  40. end
  41. cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
  42. respond_with_collection serializer: AuthorSerializer, root: :users, scope: {cache: cache, exclude_types: exclude_types}
  43. end
  44. 1 private
  45. 1 def create_action
  46. 9 @event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
  47. end
  48. 1 def accessible_records
  49. 8 PollQuery.visible_to(user: current_user, show_public: false)
  50. end
  51. end

app/controllers/api/v1/profile_controller.rb

70.21% lines covered

94 relevant lines. 66 lines covered and 28 lines missed.
    
  1. 1 class API::V1::ProfileController < API::V1::RestfulController
  2. 1 before_action :require_current_user, only: [:index, :contactable]
  3. 1 def index
  4. ids = UserQuery.invitable_user_ids(model: nil, actor: current_user, user_ids: params[:xids].split('x').map(&:to_i).compact)
  5. self.collection = User.where(id: ids)
  6. cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
  7. respond_with_collection serializer: AuthorSerializer, root: :users, scope: {cache: cache, exclude_types: exclude_types}
  8. end
  9. 1 def show
  10. 3 load_and_authorize :user
  11. 2 respond_with_resource serializer: UserSerializer
  12. end
  13. 1 def groups
  14. self.collection = GroupQuery.visible_to(user: current_user)
  15. cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
  16. respond_with_collection serializer: GroupSerializer, root: :groups, scope: {cache: cache, exclude_types: exclude_types}
  17. end
  18. 1 def time_zones
  19. time_zones = User.where('time_zone is not null').joins(:memberships).
  20. where('memberships.group_id': current_user.group_ids).
  21. group(:time_zone).count.sort_by {|k,v| -v }
  22. render json: time_zones, root: false
  23. end
  24. 1 def all_time_zones
  25. zones = ActiveSupport::TimeZone.all.map do |tz|
  26. {
  27. title: I18n.t("timezones.#{tz.name}", default: tz.name, locale: params[:selected_locale]),
  28. value: tz.tzinfo.name
  29. }
  30. end
  31. render json: zones, root: false
  32. end
  33. 1 def mentionable_users
  34. 4 instantiate_collection do |collection|
  35. 4 collection.distinct.mention_search(current_user, model, String(params[:q]).strip.delete("\u0000"))
  36. end
  37. 4 respond_with_collection serializer: AuthorSerializer, root: :users
  38. end
  39. 1 def me
  40. 2 raise CanCan::AccessDenied.new unless current_user.is_logged_in?
  41. 1 self.resource = current_user
  42. 1 respond_with_resource serializer: UserSerializer
  43. end
  44. 1 def email_api_key
  45. render json: {email_api_key: current_user.email_api_key}
  46. end
  47. 1 def reset_email_api_key
  48. current_user.update_attribute(:email_api_key, User.generate_unique_secure_token.slice(0,10))
  49. render json: {email_api_key: current_user.email_api_key}
  50. end
  51. 1 def remind
  52. service.remind(user: load_resource, actor: current_user, model: load_and_authorize(:poll))
  53. respond_with_resource
  54. end
  55. 1 def update_profile
  56. 3 service.update(**current_user_params)
  57. 2 respond_with_resource
  58. end
  59. 1 def set_volume
  60. 2 service.set_volume(user: current_user, actor: current_user, params: params.slice(:volume, :apply_to_all))
  61. 2 respond_with_resource
  62. end
  63. 1 def upload_avatar
  64. 1 service.update user: current_user, actor: current_user, params: { uploaded_avatar: params[:file], avatar_kind: :uploaded }
  65. 1 respond_with_resource
  66. end
  67. 1 def avatar_uploaded
  68. render json: {avatar_uploaded: current_user.uploaded_avatar_url}
  69. end
  70. 1 def deactivate
  71. service.deactivate(user: current_user, actor: current_user)
  72. respond_with_resource
  73. end
  74. 1 def destroy
  75. 1 service.redact(user: current_user, actor: current_user)
  76. 1 respond_with_resource
  77. end
  78. 1 def save_experience
  79. 3 raise ActionController::ParameterMissing.new(:experience) unless params.has_key?(:experience)
  80. 2 service.save_experience(user: current_user, actor: current_user, params: params)
  81. 1 respond_with_resource
  82. end
  83. 1 def email_status
  84. respond_with_resource(serializer: Pending::UserSerializer, scope: {})
  85. end
  86. 1 def email_exists
  87. render json: {email: params[:email], exists: User.where(email: params[:email]).any?}
  88. end
  89. 1 def send_merge_verification_email
  90. MergeUsersService.send_merge_verification_email(actor: current_user, target_email: params[:target_email])
  91. success_response
  92. end
  93. 1 def contactable
  94. 5 current_user.ability.authorize!(:contact, User.find(params[:user_id]))
  95. 4 success_response
  96. end
  97. 1 private
  98. 1 def current_user
  99. 10 restricted_user || super
  100. end
  101. 1 def model
  102. 4 load_and_authorize(:group, optional: true) ||
  103. load_and_authorize(:discussion, optional: true) ||
  104. load_and_authorize(:poll, optional: true) ||
  105. load_and_authorize(:comment, optional: true) ||
  106. load_and_authorize(:stance, optional: true) ||
  107. load_and_authorize(:outcome, optional: true)
  108. end
  109. 1 def accessible_records
  110. 4 resource_class
  111. end
  112. 1 def resource
  113. 30 @user || current_user.presence || user_by_email
  114. end
  115. 1 def user_by_email
  116. resource_class.active.find_by(email: params[:email]) || LoggedOutUser.new(email: params[:email])
  117. end
  118. 1 def deactivated_user
  119. resource_class.deactivated.find_by(email: params[:user][:email])
  120. end
  121. 1 def current_user_params
  122. 3 { user: current_user, actor: current_user, params: permitted_params.user }
  123. end
  124. 1 def resource_class
  125. 8 User
  126. end
  127. 1 def serializer_class
  128. 7 if current_user.restricted
  129. Restricted::UserSerializer
  130. else
  131. 7 CurrentUserSerializer
  132. end
  133. end
  134. 1 def serializer_root
  135. 10 :users
  136. end
  137. 1 def service
  138. 9 UserService
  139. end
  140. end

app/controllers/api/v1/reactions_controller.rb

86.36% lines covered

22 relevant lines. 19 lines covered and 3 lines missed.
    
  1. 1 class API::V1::ReactionsController < API::V1::RestfulController
  2. 1 alias :create :update
  3. 1 def index
  4. 2 %w[comment_ids discussion_ids outcome_ids poll_ids stance_ids].each do |key|
  5. 10 next unless params.has_key? key
  6. 6 params[key] = params[key].split('x').map(&:to_i)
  7. end
  8. 2 ReactionQuery.authorize!(user: current_user, params: params)
  9. 1 self.collection = ReactionQuery.unsafe_where(params)
  10. 1 respond_with_collection
  11. end
  12. 1 private
  13. 1 def accessible_records
  14. current_user.ability.authorize!(:show, reactable).reactions
  15. end
  16. 1 def load_resource
  17. 2 self.resource = case action_name
  18. 2 when 'create', 'update' then resource_class.find_or_initialize_by(user: current_user, reactable: reactable)
  19. else super
  20. end
  21. end
  22. 1 def reactable
  23. 2 @reactable ||= reactable_params[:reactable_type].classify.constantize.find(reactable_params[:reactable_id])
  24. end
  25. 1 def reactable_params
  26. 4 case action_name
  27. 4 when 'create', 'update' then resource_params
  28. when 'index' then params
  29. end
  30. end
  31. end

app/controllers/api/v1/received_emails_controller.rb

86.67% lines covered

30 relevant lines. 26 lines covered and 4 lines missed.
    
  1. 1 class API::V1::ReceivedEmailsController < API::V1::RestfulController
  2. 1 def index
  3. 3 raise CanCan::AccessDenied unless current_user.adminable_group_ids.include?(params[:group_id].to_i)
  4. 1 instantiate_collection
  5. 1 respond_with_collection
  6. end
  7. 1 def aliases
  8. 2 raise CanCan::AccessDenied unless current_user.adminable_group_ids.include?(params[:group_id].to_i)
  9. 1 aliases = MemberEmailAlias.where(group_id: params[:group_id])
  10. 1 render json: aliases, scope: default_scope, each_serializer: MemberEmailAliasSerializer, root: :aliases, meta: meta.merge({root: :aliases, total: collection_count})
  11. end
  12. 1 def destroy_alias
  13. member_email_alias = MemberEmailAlias.where(group_id: current_user.adminable_group_ids).find(params[:id])
  14. member_email_alias.destroy
  15. ReceivedEmailService.route_all
  16. success_response
  17. end
  18. 1 def allow
  19. 3 @received_email = ReceivedEmail.unreleased.where(group_id: current_user.adminable_group_ids).find(params[:id])
  20. 2 if @received_email.group.is_trial_or_demo?
  21. 1 respond_with_error(403, "trial groups cannot add aliases")
  22. else
  23. 1 user = @received_email.group.members.find(params[:user_id])
  24. 1 MemberEmailAlias.create(
  25. email: @received_email.sender_email,
  26. user: user,
  27. group_id: @received_email.group_id,
  28. require_dkim: @received_email.dkim_valid,
  29. require_spf: @received_email.spf_valid,
  30. author_id: current_user.id
  31. )
  32. 1 ReceivedEmailService.route(@received_email)
  33. 1 respond_with_resource
  34. end
  35. end
  36. 1 def block
  37. 2 @received_email = ReceivedEmail.unreleased.where(group_id: current_user.adminable_group_ids).find(params[:id])
  38. 1 MemberEmailAlias.create!(
  39. email: @received_email.sender_email,
  40. user_id: nil,
  41. group_id: @received_email.group_id,
  42. author_id: current_user.id
  43. )
  44. 1 @received_email.update(group_id: nil)
  45. 1 respond_with_resource
  46. end
  47. 1 private
  48. 1 def accessible_records
  49. 1 ReceivedEmail.where(group_id: params[:group_id], released: false)
  50. end
  51. end

app/controllers/api/v1/registrations_controller.rb

96.67% lines covered

30 relevant lines. 29 lines covered and 1 lines missed.
    
  1. 1 class API::V1::RegistrationsController < Devise::RegistrationsController
  2. 1 include LocalesHelper
  3. 1 before_action :configure_permitted_parameters
  4. 1 before_action :permission_check, only: :create
  5. 1 def create
  6. 10 @email_can_be_verified = email_can_be_verified?
  7. 10 self.resource = UserService.create(params: sign_up_params)
  8. 9 if !resource.errors.any?
  9. 7 save_detected_locale(resource)
  10. 7 if @email_can_be_verified
  11. 2 sign_in resource
  12. 2 flash[:notice] = t(:'devise.sessions.signed_in')
  13. 2 render json: Boot::User.new(resource).payload.merge({ success: :ok, signed_in: true })
  14. else
  15. 5 LoginTokenService.create(actor: resource, uri: URI::parse(request.referrer.to_s))
  16. 5 render json: { success: :ok, signed_in: false }
  17. end
  18. 7 EventBus.broadcast('registration_create', resource)
  19. else
  20. 2 render json: { errors: resource.errors }, status: 422
  21. end
  22. rescue UserService::EmailTakenError => e
  23. 1 render json: {errors: {email: [I18n.t('auth_form.email_taken')]}}, status: 422
  24. end
  25. 1 private
  26. 1 def email_can_be_verified?
  27. 10 (pending_membership&.user ||
  28. pending_login_token&.user ||
  29. pending_discussion_reader&.user ||
  30. pending_stance&.user ||
  31. pending_identity)&.email == sign_up_params[:email]
  32. end
  33. 1 def pending_user
  34. 2 user = (pending_membership || pending_login_token || pending_identity)&.user
  35. 2 user if user && !user.email_verified?
  36. end
  37. 1 def permission_check
  38. 10 if !(AppConfig.app_features[:create_user] || pending_invitation || pending_group)
  39. render json: { errors: {email: [I18n.t('auth_form.invitation_required')], name: [I18n.t('auth_form.invitation_required')]}}, status: 422
  40. end
  41. end
  42. 1 def configure_permitted_parameters
  43. 10 devise_parameter_sanitizer.permit(:sign_up) do |u|
  44. 20 u.permit(:name, :email, :recaptcha, :legal_accepted, :email_newsletter)
  45. end
  46. end
  47. end

app/controllers/api/v1/reports_controller.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. class API::V1::ReportsController < API::V1::RestfulController
  2. def index
  3. start_at = Date.parse(params.fetch(:start_month, 12.months.ago.to_date.iso8601[0..-4]) + "-01")
  4. end_at = Date.parse(params.fetch(:end_month, Date.today.iso8601[0..-4]) + "-01") + 1.month
  5. interval = params.fetch(:interval, 'month')
  6. user_group_ids = current_user.group_ids
  7. group_ids = params.fetch(:group_ids).split(',').map(&:to_i)
  8. all_group_ids = Group.where("id IN (:group_ids) OR parent_id IN (:group_ids)", group_ids: Group.where(id: group_ids).pluck(:id, :parent_id).flatten.uniq).pluck(:id).uniq
  9. all_groups = Group.where(id: all_group_ids).order("parent_id NULLS FIRST, name asc").pluck(:id, :name).map {|pair| {id: pair[0], name: pair[1] } }
  10. if current_user.is_admin?
  11. all_groups.unshift({id: 0, name: 'Direct threads'})
  12. else
  13. group_ids = group_ids & current_user.group_ids
  14. all_group_ids = all_group_ids & current_user.group_ids
  15. end
  16. @report = ReportService.new(interval: interval, group_ids: group_ids, start_at: start_at, end_at: end_at)
  17. render json: {
  18. all_groups: all_groups,
  19. intervals: @report.intervals,
  20. comments_per_interval: @report.comments_per_interval,
  21. discussions_per_interval: @report.discussions_per_interval,
  22. polls_per_interval: @report.polls_per_interval,
  23. stances_per_interval: @report.stances_per_interval,
  24. outcomes_per_interval: @report.outcomes_per_interval,
  25. discussions_count: @report.discussions_count,
  26. discussions_with_polls_count: @report.discussions_with_polls_count,
  27. polls_count: @report.polls_count,
  28. polls_with_outcomes_count: @report.polls_with_outcomes_count,
  29. tag_names: @report.tag_names,
  30. discussion_tag_counts: @report.discussion_tag_counts,
  31. poll_tag_counts: @report.poll_tag_counts,
  32. tag_counts: @report.tag_counts,
  33. users: @report.users.map {|u| {id: u.id, name: u.name, country: u.country} },
  34. discussions_per_user: @report.discussions_per_user,
  35. comments_per_user: @report.comments_per_user,
  36. polls_per_user: @report.polls_per_user,
  37. outcomes_per_user: @report.outcomes_per_user,
  38. stances_per_user: @report.stances_per_user,
  39. reactions_per_user: @report.reactions_per_user,
  40. countries: @report.countries,
  41. discussions_per_country: @report.discussions_per_country,
  42. comments_per_country: @report.comments_per_country,
  43. polls_per_country: @report.polls_per_country,
  44. outcomes_per_country: @report.outcomes_per_country,
  45. stances_per_country: @report.stances_per_country,
  46. reactions_per_country: @report.reactions_per_country,
  47. users_per_country: @report.users_per_country,
  48. total_users: @report.users_per_country.values.sum.to_f,
  49. }
  50. end
  51. end

app/controllers/api/v1/restful_controller.rb

94.12% lines covered

17 relevant lines. 16 lines covered and 1 lines missed.
    
  1. 1 class API::V1::RestfulController < API::V1::SnorlaxBase
  2. 1 include ActiveStorage::SetCurrent
  3. 1 include ::LocalesHelper
  4. 1 include ::ProtectedFromForgery
  5. 1 include ::LoadAndAuthorize
  6. 1 include ::CurrentUserHelper
  7. 1 include ::SentryHelper
  8. 1 include ::PendingActionsHelper
  9. 1 before_action :handle_pending_actions
  10. 1 around_action :use_preferred_locale # LocalesHelper
  11. 1 before_action :set_paper_trail_whodunnit # gem 'paper_trail'
  12. 1 before_action :set_sentry_context # SentryHelper
  13. 1 before_action :deny_spam_users # CurrentUserHelper
  14. 1 private
  15. 1 def require_current_user
  16. 5 unless current_user && current_user.is_logged_in?
  17. render(json: {error: 'you gotta be signed in'}, root: false, status: 401)
  18. end
  19. end
  20. end

app/controllers/api/v1/search_controller.rb

85.96% lines covered

57 relevant lines. 49 lines covered and 8 lines missed.
    
  1. 1 class API::V1::SearchController < API::V1::RestfulController
  2. 1 def index
  3. 4 if group_or_org_id.to_i == 0
  4. 2 rel = PgSearch.multisearch(params[:query]).where("group_id is null and discussion_id IN (:discussion_ids)", discussion_ids: current_user.guest_discussion_ids)
  5. end
  6. 4 if group_or_org_id.to_i > 0
  7. 2 rel = PgSearch.multisearch(params[:query]).where("group_id IN (:group_ids)", group_ids: group_ids)
  8. end
  9. 4 if group_or_org_id.blank?
  10. 1 rel = PgSearch.multisearch(params[:query]).where("group_id IN (:group_ids) OR discussion_id in (:discussion_ids)", group_ids: group_ids, discussion_ids: current_user.guest_discussion_ids)
  11. end
  12. 4 if params[:tag]
  13. discussion_ids = Discussion.where(group_id: group_ids).where("tags @> ARRAY[?]::varchar[]", Array(params[:tag])).pluck(:id)
  14. poll_ids = Poll.where(group_id: group_ids).where("tags @> ARRAY[?]::varchar[]", Array(params[:tag])).pluck(:id)
  15. rel = rel.where("discussion_id in (:discussion_ids) or poll_id in (:poll_ids)", discussion_ids: discussion_ids, poll_ids: poll_ids)
  16. end
  17. 4 if %w[Discussion Comment Poll Stance Outcome].include?(params[:type])
  18. rel = rel.where(searchable_type: params[:type])
  19. end
  20. 4 if params[:order] == 'authored_at_desc'
  21. rel = rel.reorder('authored_at desc')
  22. end
  23. 4 if params[:order] == 'authored_at_asc'
  24. rel = rel.reorder('authored_at asc')
  25. end
  26. 4 results = rel.limit(20).with_pg_search_highlight.all
  27. # results = results.order().offset().limit()
  28. 4 groups = access_by_id(Group.where(id: results.map(&:group_id)))
  29. 4 discussions = access_by_id(Discussion.where(id: results.map(&:discussion_id)))
  30. 4 polls = access_by_id(Poll.where(id: results.map(&:poll_id)))
  31. 4 authors = access_by_id(User.where(id: results.map(&:author_id)))
  32. 4 poll_events = access_by_id(
  33. Event.where("discussion_id is not null").where(eventable_type: "Poll", eventable_id: results.map(&:poll_id)),
  34. :eventable_id
  35. )
  36. 4 stance_events = access_by_id(
  37. 30 Event.where("discussion_id is not null").where(eventable_type: "Stance", eventable_id: results.filter {|r| r.searchable_type == 'Stance'}.map(&:searchable_id)),
  38. :eventable_id
  39. )
  40. 4 self.collection = results.map do |res|
  41. 30 poll = polls[res.poll_id]
  42. 30 discussion = discussions[res.discussion_id]
  43. 30 group = groups[res.group_id]
  44. 30 author = authors[res.author_id]
  45. 30 sequence_id = ((res.searchable_type == "Stance" && stance_events[res.searchable_id]) || poll_events[res.poll_id] || nil)&.sequence_id
  46. 30 SearchResult.new(
  47. id: res.id,
  48. searchable_type: res.searchable_type,
  49. searchable_id: res.searchable_id,
  50. poll_title: poll&.title,
  51. discussion_title: discussion&.title,
  52. discussion_key: discussion&.key,
  53. highlight: res.pg_search_highlight,
  54. poll_id: res.poll_id,
  55. poll_key: poll&.key,
  56. sequence_id: sequence_id,
  57. group_handle: group&.handle,
  58. group_key: group&.key,
  59. group_id: group&.id,
  60. group_name: group&.full_name,
  61. author_name: author&.name,
  62. author_id: res.author_id,
  63. authored_at: res.authored_at,
  64. 30 tags: (Array(poll&.tags) + Array(discussion&.tags)).uniq
  65. )
  66. end
  67. 4 respond_with_collection
  68. end
  69. 1 private
  70. 1 def access_by_id(collection, id_col = 'id')
  71. 24 h = {}
  72. 24 collection.each do |row|
  73. 58 h[row.send(id_col)] = row
  74. end
  75. 24 h
  76. end
  77. 1 def exclude_types
  78. 8 'group membership discussion outcome event'.split(' ')
  79. end
  80. 1 def group_ids
  81. 3 if params[:group_id].present?
  82. 2 current_user.browseable_group_ids & Array(params[:group_id].to_i)
  83. 1 elsif params[:org_id] == '0'
  84. []
  85. 1 elsif params[:org_id].present?
  86. current_user.browseable_group_ids & Group.find(params[:org_id]).id_and_subgroup_ids
  87. else
  88. 1 current_user.browseable_group_ids
  89. end
  90. end
  91. 1 def group_or_org_id
  92. 12 params[:group_id] || params[:org_id]
  93. end
  94. 1 def serializer_root
  95. 4 :search_results
  96. end
  97. 1 def serializer_class
  98. 4 SearchResultSerializer
  99. end
  100. end

app/controllers/api/v1/sessions_controller.rb

65.79% lines covered

38 relevant lines. 25 lines covered and 13 lines missed.
    
  1. 1 class API::V1::SessionsController < Devise::SessionsController
  2. 1 include PrettyUrlHelper
  3. 1 before_action :configure_permitted_parameters
  4. 1 def create
  5. 9 if user = attempt_login
  6. 4 sign_in(user)
  7. 4 flash[:notice] = t(:'devise.sessions.signed_in')
  8. 4 user.update(name: resource_params[:name]) if resource_params[:name]
  9. 4 render json: Boot::User.new(user).payload
  10. 4 EventBus.broadcast('session_create', user)
  11. else
  12. 5 render json: { errors: failure_message }, status: 401
  13. end
  14. 9 session.delete(:pending_login_token)
  15. end
  16. 1 def destroy
  17. sign_out resource_name
  18. # temp fix because we've changed the session domain
  19. if ENV['CANONICAL_HOST'] == 'www.loomio.org'
  20. cookies.delete :_loomio, domain: '.loomio.org'
  21. cookies.delete :remember_user_token, domain: '.loomio.org'
  22. cookies.delete :_loomio
  23. cookies.delete :remember_user_token
  24. end
  25. flash[:notice] = t(:'devise.sessions.signed_out')
  26. render json: { success: :ok }
  27. end
  28. 1 private
  29. 1 def failure_message
  30. 5 if resource_params[:password] && User.where(email: resource_params[:email]).where.not(locked_at: nil).exists?
  31. { password: [:'auth_form.account_locked'] }
  32. else
  33. 5 { password: [:'auth_form.invalid_password'] }
  34. end
  35. end
  36. 1 def attempt_login
  37. 9 if pending_login_token&.useable?
  38. 3 pending_login_token.user
  39. 6 elsif resource_params[:code]
  40. login_token_user
  41. else
  42. 6 warden.authenticate(scope: resource_name)
  43. end
  44. end
  45. 1 def login_token_user
  46. token = LoginToken.unused.find_by(code: resource_params.require(:code))
  47. token.user if token&.user&.email == resource_params.require(:email)
  48. end
  49. 1 def configure_permitted_parameters
  50. 9 devise_parameter_sanitizer.permit(:sign_in) do |u|
  51. u.permit(:code, :email, :password, :remember_me)
  52. end
  53. end
  54. end

app/controllers/api/v1/snorlax_base.rb

91.67% lines covered

156 relevant lines. 143 lines covered and 13 lines missed.
    
  1. 1 class API::V1::SnorlaxBase < ActionController::Base
  2. 105 rescue_from(CanCan::AccessDenied) { |e| respond_with_standard_error e, 403 }
  3. 2 rescue_from(Subscription::MaxMembersExceeded) { |e| respond_with_standard_error e, 403 }
  4. 6 rescue_from(ActionController::UnpermittedParameters) { |e| respond_with_standard_error e, 400 }
  5. 3 rescue_from(ActionController::ParameterMissing) { |e| respond_with_standard_error e, 400 }
  6. 9 rescue_from(ActiveRecord::RecordNotFound) { |e| respond_with_standard_error e, 404 }
  7. 4 rescue_from(ActiveRecord::RecordInvalid) { |e| respond_with_errors }
  8. 1 attr_accessor :collection_count
  9. 1 def show
  10. respond_with_resource
  11. end
  12. 1 def index
  13. 8 instantiate_collection
  14. 7 respond_with_collection
  15. end
  16. 1 def create
  17. 41 instantiate_resource
  18. 41 create_action
  19. 25 create_response
  20. end
  21. 1 def update
  22. 38 load_resource
  23. 38 update_action
  24. 25 update_response
  25. end
  26. 1 def destroy
  27. 3 load_resource
  28. 3 destroy_action
  29. 1 destroy_response
  30. end
  31. 1 private
  32. 1 def load_resource
  33. if resource_class.respond_to?(:friendly)
  34. self.resource = resource_class.friendly.find(params[:id])
  35. else
  36. self.resource = resource_class.find(params[:id])
  37. end
  38. end
  39. 1 def create_action
  40. 23 @event = service.create(**{resource_symbol => resource, actor: current_user})
  41. end
  42. 1 def update_action
  43. 39 @event = service.update(**{resource_symbol => resource, params: resource_params, actor: current_user})
  44. end
  45. 1 def destroy_action
  46. 3 service.destroy(**{resource_symbol => resource, actor: current_user})
  47. end
  48. 1 def permitted_params
  49. 155 @permitted_params ||= PermittedParams.new(params)
  50. end
  51. 1 def service
  52. 147 "#{resource_name}_service".camelize.constantize
  53. end
  54. 1 def public_records
  55. resource_class.visible_to_public.order(created_at: :desc)
  56. end
  57. 1 def respond_with_resource(scope: default_scope, serializer: serializer_class, root: serializer_root)
  58. 127 if resource.errors.empty?
  59. 120 respond_with_collection scope: scope, serializer: serializer, root: root
  60. else
  61. 7 respond_with_errors
  62. end
  63. end
  64. 1 def respond_ok
  65. 2 render json: {}, status: 200
  66. end
  67. 1 def respond_with_collection(scope: default_scope, serializer: serializer_class, root: serializer_root)
  68. 223 render json: records_to_serialize, scope: scope, each_serializer: serializer, root: root, meta: meta.merge({root: root, total: collection_count})
  69. end
  70. 1 def meta
  71. 224 @meta || {}
  72. end
  73. 1 def add_meta(key, value)
  74. 3 @meta ||= {}
  75. 3 @meta[key] = value
  76. end
  77. # prefer this
  78. 1 def records_to_serialize
  79. 780 if @event.is_a?(Event)
  80. 205 Array(@event)
  81. else
  82. 575 collection || Array(resource)
  83. end
  84. end
  85. 1 def serializer_class
  86. 160 record = records_to_serialize.first
  87. 160 if record.nil?
  88. 3 EventSerializer
  89. 157 elsif record.is_a? Event
  90. 58 EventSerializer
  91. else
  92. 99 "#{record.class}Serializer".constantize
  93. end
  94. end
  95. 1 def serializer_root
  96. 167 record = records_to_serialize.first
  97. 167 if record.nil?
  98. 3 controller_name
  99. 164 elsif record.is_a? Event
  100. 58 'events'
  101. else
  102. 106 record.class.to_s.underscore.pluralize
  103. end
  104. end
  105. 1 def default_scope
  106. {
  107. 230 cache: RecordCache.for_collection(records_to_serialize, current_user.id, exclude_types),
  108. current_user_id: current_user.id,
  109. exclude_types: exclude_types
  110. }
  111. end
  112. 1 def exclude_types
  113. 407 params[:exclude_types].to_s.split(' ')
  114. end
  115. # phase this out
  116. 1 def events_to_serialize
  117. return [] unless @event.is_a?(Event)
  118. Array(@event)
  119. end
  120. # phase this out
  121. 1 def resources_to_serialize
  122. 1 collection || Array(resource)
  123. end
  124. 1 def collection
  125. 802 instance_variable_get :"@#{resource_name.pluralize}"
  126. end
  127. 1 def resource
  128. 498 instance_variable_get :"@#{resource_name}"
  129. end
  130. 1 def resource=(value)
  131. 167 instance_variable_set :"@#{resource_name}", value
  132. end
  133. 1 def collection=(value)
  134. 278 instance_variable_set :"@#{resource_name.pluralize}", value
  135. end
  136. 1 def instantiate_resource
  137. 67 self.resource = resource_class.new(self.class.filter_params(resource_class, resource_params))
  138. end
  139. 1 def self.filter_params(resource_class, resource_params)
  140. 207 newbie = resource_class.new
  141. 207 out = {}.with_indifferent_access
  142. 207 resource_params.each_pair do |k, v|
  143. 560 out[k.to_sym] = v if newbie.respond_to?("#{k}=")
  144. end
  145. 207 out
  146. end
  147. 1 def instantiate_collection
  148. 47 self.collection = accessible_records
  149. 45 self.collection = yield collection if block_given?
  150. 45 self.collection = timeframe_collection collection
  151. 45 self.collection_count = collection.count
  152. 45 self.collection = page_collection collection
  153. 45 self.collection = order_collection collection
  154. end
  155. 1 def timeframe_collection(collection)
  156. 45 if resource_class.try(:has_timeframe?) && (params[:since] || params[:until])
  157. 4 parse_date_parameters # I feel like Rails should do this for me..
  158. 4 collection.within(params[:since], params[:until], params[:timeframe_for])
  159. else
  160. 41 collection
  161. end
  162. end
  163. 1 def parse_date_parameters
  164. 12 %w(since until).each { |field| params[field] = DateTime.parse(params[field].to_s) if params[field] }
  165. end
  166. 1 def page_collection(collection)
  167. 45 collection.offset(params[:from].to_i).limit((params[:per] || default_page_size).to_i)
  168. end
  169. 1 def order_collection(collection)
  170. 45 if valid_orders.include?(params[:order])
  171. collection.order(params[:order])
  172. else
  173. 45 collection
  174. end
  175. end
  176. 1 def accessible_records
  177. if current_user.is_logged_in?
  178. visible_records
  179. else
  180. public_records
  181. end
  182. end
  183. 1 def visible_records
  184. raise NotImplementedError.new
  185. end
  186. 1 def valid_orders
  187. 39 []
  188. end
  189. 1 def public_records
  190. raise NotImplementedError.new
  191. end
  192. 1 def default_page_size
  193. 44 50
  194. end
  195. 1 def update_response
  196. 26 respond_with_resource
  197. end
  198. 1 def create_response
  199. 24 respond_with_resource
  200. end
  201. 1 def destroy_response
  202. 1 success_response
  203. end
  204. 1 def success_response
  205. 5 render json: {success: 'success'}
  206. end
  207. 1 def error_response(status = 500)
  208. 3 render json: {error: status}, root: false, status: status
  209. end
  210. 1 def load_resource
  211. 85 self.resource = resource_class.find(params[:id])
  212. end
  213. 1 def resource_params
  214. 161 permitted_params.send resource_name
  215. end
  216. 1 def resource_symbol
  217. 98 resource_name.to_sym
  218. end
  219. 1 def resource_name
  220. 2424 controller_name.singularize
  221. end
  222. 1 def resource_class
  223. 263 resource_name.camelize.constantize
  224. end
  225. 1 def respond_with_standard_error(error, status)
  226. 120 render json: {exception: error.class, error: error.to_s}, root: false, status: status
  227. end
  228. 1 def respond_with_error(status, message = "error")
  229. 1 render json: {error: message}, root: false, status: status
  230. end
  231. 1 def respond_with_errors(record = resource)
  232. 11 render json: {errors: record.errors.as_json}, root: false, status: 422
  233. end
  234. end

app/controllers/api/v1/stances_controller.rb

86.67% lines covered

75 relevant lines. 65 lines covered and 10 lines missed.
    
  1. 1 class API::V1::StancesController < API::V1::RestfulController
  2. 1 def create
  3. 12 super
  4. rescue ActiveRecord::RecordNotUnique
  5. 1 self.resource = resource_class.find_by!(
  6. poll_id: params[:stance][:poll_id],
  7. participant_id: current_user.id)
  8. 1 update_action
  9. 1 update_response
  10. end
  11. 1 def uncast
  12. 3 @stance = current_user.stances.latest.find(params[:id])
  13. 2 StanceService.uncast(stance: @stance, actor: current_user)
  14. 1 respond_with_recent_stances
  15. end
  16. 1 def index
  17. 3 instantiate_collection do |collection|
  18. 2 if !@poll.anonymous && name = params[:name].presence
  19. collection = collection.
  20. joins('LEFT OUTER JOIN users on stances.participant_id = users.id').
  21. where(latest: true, revoked_at: nil).
  22. where("users.name ilike :first OR
  23. users.name ilike :last OR
  24. users.email ilike :first OR
  25. users.username ilike :first",
  26. first: "#{name}%", last: "% #{name}%")
  27. end
  28. 2 if @poll.show_results?(voted: true)
  29. 2 if poll_option_id = params[:poll_option_id].presence
  30. collection = collection.joins(:poll_options).where("poll_options.id" => poll_option_id)
  31. end
  32. end
  33. 2 collection.order('cast_at DESC NULLS LAST, created_at DESC')
  34. end
  35. 2 respond_with_collection
  36. end
  37. 1 def users
  38. 1 instantiate_collection do |collection|
  39. 1 if query = params[:query]
  40. collection = collection.
  41. joins('LEFT OUTER JOIN users on stances.participant_id = users.id').
  42. where("users.name ilike :first OR
  43. users.name ilike :last OR
  44. users.email ilike :first OR
  45. users.username ilike :first",
  46. first: "#{query}%", last: "% #{query}%")
  47. end
  48. 1 user_ids = collection.pluck(:participant_id)
  49. 1 self.add_meta :guest_ids, collection.where(guest: true).pluck(:participant_id) & user_ids
  50. 1 self.add_meta :member_admin_ids, @poll.group.admins.pluck(:user_id) & user_ids
  51. 1 self.add_meta :stance_admin_ids, collection.where(admin: true).pluck(:participant_id) & user_ids
  52. 1 User.where(id: collection.pluck(:participant_id))
  53. end
  54. 1 respond_with_collection serializer: AuthorSerializer
  55. end
  56. 1 def my_stances
  57. self.collection = current_user.stances.latest.includes({poll: :discussion})
  58. self.collection = collection.where('polls.discussion_id': @discussion.id) if load_and_authorize(:discussion, optional: true)
  59. self.collection = collection.where('discussions.group_id': @group.id) if load_and_authorize(:group, optional: true)
  60. respond_with_collection
  61. end
  62. 1 def make_admin
  63. 3 @stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
  64. 3 current_user.ability.authorize! :make_admin, @stance
  65. 1 @stance.update(admin: true)
  66. 1 respond_with_resource
  67. end
  68. 1 def remove_admin
  69. 1 @stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
  70. 1 current_user.ability.authorize! :remove_admin, @stance
  71. 1 @stance.update(admin: false)
  72. 1 @stance.poll.update_counts!
  73. 1 respond_with_resource
  74. end
  75. 1 def revoke
  76. 2 @stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
  77. 2 current_user.ability.authorize! :remove, @stance
  78. # revoke all stances, not just the latest one
  79. 1 Stance.where(revoked_at: nil, participant_id: params[:participant_id], poll_id: params[:poll_id]).
  80. update_all(revoked_at: Time.zone.now, revoker_id: current_user.id)
  81. 1 @stance.reload
  82. 1 @stance.poll.update_counts!
  83. 1 @stances = @stance.poll.stances.where(participant_id: params[:participant_id])
  84. 1 live_update_outdated_stances(@stance.poll)
  85. 1 respond_with_collection
  86. end
  87. 1 private
  88. 1 def live_update_outdated_stances(poll)
  89. 1 return if poll.discussion.nil?
  90. # want to find stances with comments
  91. stance_ids = poll.discussion.items.where(
  92. eventable_type: 'Stance',
  93. eventable_id: poll.stances.with_reason.where(latest: false).pluck(:id)
  94. ).where("child_count > 0").pluck('eventable_id')
  95. stances = Stance.where(id: stance_ids).order('id desc').limit(50)
  96. MessageChannelService.publish_models(stances, group_id: poll.group_id, user_id: current_user.id)
  97. end
  98. 1 def respond_with_recent_stances
  99. 1 @event = nil
  100. 1 @stances = @stance.poll.stances.where(revoked_at: nil, participant_id: current_user.id).order('id desc').limit(10)
  101. 1 respond_with_collection
  102. end
  103. 1 def current_user_is_admin?
  104. 16 stance = Stance.find_by(id: params[:id])
  105. 16 poll = Poll.find_by(id: params[:poll_id])
  106. 16 return false unless (stance || poll)
  107. 12 (stance || poll).poll.admins.exists?(current_user.id)
  108. end
  109. 1 def exclude_types
  110. 32 %w[group discussion]
  111. end
  112. 1 def default_scope
  113. 16 super.merge({include_email: current_user_is_admin?})
  114. end
  115. 1 def accessible_records
  116. 4 load_and_authorize(:poll).stances.latest
  117. end
  118. end

app/controllers/api/v1/tags_controller.rb

55.0% lines covered

20 relevant lines. 11 lines covered and 9 lines missed.
    
  1. 1 class API::V1::TagsController < API::V1::RestfulController
  2. 1 def priority
  3. load_and_authorize_group
  4. Array(params[:ids]).each_with_index do |id, index|
  5. Tag.where(id: id, group_id: @group.id).update_all(priority: index)
  6. end
  7. instantiate_collection
  8. # rember to live update too
  9. respond_with_collection
  10. end
  11. 1 private
  12. 1 def respond_with_group
  13. 1 self.resource = resource.group.reload
  14. 1 respond_with_resource
  15. end
  16. 1 def create_response
  17. 1 respond_with_group
  18. end
  19. 1 def destroy_response
  20. respond_with_group
  21. end
  22. 1 def accessible_records
  23. Tag.where(group_id: @group.id)
  24. end
  25. 1 def load_and_authorize_group
  26. @group = Group.find(params[:group_id])
  27. current_user.ability.authorize!(:update, @group)
  28. end
  29. end

app/controllers/api/v1/tasks_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 class API::V1::TasksController < API::V1::RestfulController
  2. 1 def index
  3. # return tasks
  4. 1 self.collection = Task.joins('left outer join tasks_users on tasks_users.task_id = tasks.id')
  5. .where("author_id = :user_id OR doer_id = :user_id OR tasks_users.user_id = :user_id", user_id: current_user.id)
  6. 1 respond_with_collection
  7. end
  8. 1 def update_done
  9. 2 @task = Task.find_by(record: record, uid: params[:uid])
  10. 2 current_user.ability.authorize!(:update, @task)
  11. 2 TaskService.update_done(@task, current_user, params[:done] == 'true')
  12. 2 respond_with_resource
  13. # we should also serialize the assocated record
  14. end
  15. 1 def mark_as_done
  16. 2 @task = Task.find(params[:id])
  17. 2 current_user.ability.authorize!(:update, @task)
  18. 2 TaskService.update_done(@task, current_user, true)
  19. 2 respond_with_resource
  20. # we should also serialize the assocated record
  21. end
  22. 1 def mark_as_not_done
  23. 1 @task = Task.find(params[:id])
  24. 1 current_user.ability.authorize!(:update, @task)
  25. 1 TaskService.update_done(@task, current_user, false)
  26. 1 respond_with_resource
  27. end
  28. 1 private
  29. 1 def record
  30. 2 load_and_authorize(:group, optional: true) ||
  31. load_and_authorize(:discussion, optional: true) ||
  32. load_and_authorize(:comment, optional: true) ||
  33. load_and_authorize(:poll, optional: true) ||
  34. load_and_authorize(:outcome, optional: true)
  35. end
  36. end

app/controllers/api/v1/translations_controller.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class API::V1::TranslationsController < API::V1::RestfulController
  2. def inline
  3. self.resource = service.create(model: load_and_authorize(params[:model]), to: params[:to])
  4. respond_with_resource
  5. end
  6. end

app/controllers/api/v1/trials_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 class API::V1::TrialsController < API::V1::RestfulController
  2. 1 def create
  3. 1 email = params[:user_email].strip
  4. 1 user = User.verified.find_by(email: email)
  5. 1 if !user
  6. 1 user = User.where(email_verified: false, email: email).first_or_create
  7. 1 user.name = params[:user_name].strip
  8. 1 user.recaptcha = params[:recaptcha]
  9. 1 user.legal_accepted = true
  10. 1 user.email_newsletter = !!params[:newsletter]
  11. # user.require_valid_signup = true
  12. # user.require_recaptcha = true
  13. 1 user.save!
  14. end
  15. 1 raise "you said I'd have a user by now" unless user && user.valid?
  16. 1 group = Group.new
  17. 1 group.assign_attributes_and_files(params.require(:group).permit(permitted_params.group_attributes))
  18. 1 group.group_privacy = "secret"
  19. 1 group.category = params[:group_category]
  20. 1 group.info['how_did_you_hear_about_loomio'] = params[:how_did_you_hear_about_loomio]
  21. 1 group.handle = GroupService.suggest_handle(name: group.name, parent_handle: nil)
  22. 1 GroupService.create(group: group, actor: user, skip_authorize: true)
  23. 1 raise "start trial failed" unless group.valid?
  24. 1 group_path = group.handle ? group_handle_path(group.handle) : group_path(group)
  25. 1 render json: {success: :ok, group_path: group_path}
  26. end
  27. end

app/controllers/api/v1/versions_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class API::V1::VersionsController < API::V1::RestfulController
  2. 1 def show
  3. 2 self.resource = model.versions[params[:index].to_i]
  4. 1 respond_with_resource
  5. end
  6. 1 private
  7. 1 def exclude_types
  8. 2 %w[discussion group user comment poll stance stance_choice]
  9. end
  10. 1 def serializer_class
  11. 1 VersionSerializer
  12. end
  13. 1 def serializer_root
  14. 1 'versions'
  15. end
  16. 1 def model
  17. 2 load_and_authorize(:group, optional:true) ||
  18. load_and_authorize(:discussion, optional:true) ||
  19. load_and_authorize(:comment, optional:true) ||
  20. load_and_authorize(:stance, optional:true) ||
  21. load_and_authorize(:poll, optional:true) ||
  22. load_and_authorize(:outcome, optional:false)
  23. end
  24. end

app/controllers/application_controller.rb

0.0% lines covered

152 relevant lines. 0 lines covered and 152 lines missed.
    
  1. class ApplicationController < ActionController::Base
  2. include LocalesHelper
  3. include ProtectedFromForgery
  4. include CurrentUserHelper
  5. include SentryHelper
  6. include PrettyUrlHelper
  7. include LoadAndAuthorize
  8. include EmailHelper
  9. include ApplicationHelper
  10. helper :email
  11. helper :formatted_date
  12. around_action :process_time_zone # LocalesHelper
  13. around_action :use_preferred_locale # LocalesHelper
  14. before_action :deny_spam_users # CurrentUserHelper
  15. before_action :set_last_seen_at # CurrentUserHelper
  16. before_action :handle_pending_actions # PendingActionsHelper
  17. before_action :set_sentry_context
  18. before_action :ensure_canonical_host
  19. helper_method :current_user
  20. helper_method :current_version
  21. helper_method :bundle_asset_path
  22. helper_method :supported_locales
  23. helper_method :is_old_browser?
  24. skip_before_action :verify_authenticity_token, only: :bug_tunnel
  25. caches_page :sitemap
  26. rescue_from(ActionController::UnknownFormat) do
  27. respond_with_error message: :"errors.not_found", status: 404
  28. end
  29. rescue_from(ActionView::MissingTemplate) do |exception|
  30. raise exception unless %w[txt text gif png].include?(params[:format])
  31. end
  32. rescue_from(ActiveRecord::RecordNotFound) do
  33. respond_with_error message: :"errors.not_found", status: 404
  34. end
  35. rescue_from(Membership::InvitationAlreadyUsed) do |exception|
  36. session.delete(:pending_membership_token)
  37. if current_user.ability.can?(:show, exception.membership.group)
  38. redirect_to polymorphic_path(exception.membership.group) || dashboard_path
  39. else
  40. respond_with_error message: :"invitation.invitation_already_used"
  41. end
  42. end
  43. rescue_from(CanCan::AccessDenied) do |exception|
  44. if current_user.is_logged_in?
  45. flash[:error] = t("error.access_denied")
  46. redirect_to dashboard_path
  47. else
  48. authenticate_user!
  49. end
  50. end
  51. def response_format
  52. params[:format] == 'json' ? :json : :html
  53. end
  54. def respond_with_error(message: nil, status: 400)
  55. @title = t("errors.#{status}.body")
  56. @body = t(message || "errors.#{status}.body")
  57. render "application/error", layout: 'basic', status: status, formats: response_format
  58. end
  59. def index
  60. boot_app
  61. end
  62. def sitemap
  63. @entries = []
  64. Group.published.where(is_visible_to_public: true).each do |g|
  65. @entries << [url_for(g), g.updated_at.to_date.iso8601]
  66. end
  67. Discussion.visible_to_public.joins(:group).where('groups.archived_at is null').each do |d|
  68. @entries << [url_for(d), d.last_activity_at.to_date.iso8601]
  69. end
  70. end
  71. def show
  72. resource = ModelLocator.new(resource_name, params).locate!
  73. @recipient = current_user
  74. if current_user.can? :show, resource
  75. assign_resource
  76. @pagination = pagination_params
  77. respond_to do |format|
  78. format.html
  79. format.rss { render :"show.xml" }
  80. format.xml
  81. end
  82. else
  83. boot_app(status: 403)
  84. end
  85. end
  86. def crowdfunding
  87. render layout: 'basic'
  88. end
  89. def brand
  90. render layout: 'basic'
  91. end
  92. def bug_tunnel
  93. raise "no sentry dsn" unless ENV['SENTRY_PUBLIC_DSN']
  94. uri = URI(ENV['SENTRY_PUBLIC_DSN'])
  95. known_host = uri.host
  96. known_project_id = uri.path.tr('/', '')
  97. envelope = request.body.read
  98. piece = envelope.split("\n").first
  99. header = JSON.parse(piece)
  100. dsn = URI.parse(header['dsn'])
  101. project_id = dsn.path.tr('/', '')
  102. raise "Invalid sentry hostname: #{dsn.hostname}" if dsn.hostname != known_host
  103. raise "Invalid sentry project id: #{project_id}" if project_id != known_project_id
  104. upstream_sentry_url = "https://#{known_host}/api/#{known_project_id}/envelope/"
  105. Net::HTTP.post(URI(upstream_sentry_url), envelope)
  106. head(:ok)
  107. rescue => e
  108. # handle exception in your preferred style,
  109. # e.g. by logging or forwarding to server Sentry project
  110. Rails.logger.error('error tunneling to sentry')
  111. end
  112. def ok
  113. head :ok
  114. end
  115. protected
  116. def pagination_params
  117. default_limit = (params[:export]) ? 2000 : 10
  118. { limit: params.fetch(:limit, default_limit).to_i, offset: params.fetch(:offset, 0).to_i }
  119. end
  120. def prevent_caching
  121. response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' # HTTP 1.1.
  122. response.headers['Pragma'] = 'no-cache' # HTTP 1.0.
  123. response.headers['Expires'] = '0' # Proxies.
  124. end
  125. private
  126. def ensure_canonical_host
  127. if ENV['REDIRECT_TO_CANONICAL_HOST']
  128. if request.host != ENV['CANONICAL_HOST']
  129. u = URI(request.url)
  130. u.host = ENV['CANONICAL_HOST']
  131. redirect_to u.to_s, status: :moved_permanently
  132. end
  133. end
  134. end
  135. def boot_app(status: 200)
  136. expires_now
  137. prevent_caching
  138. render file: Rails.root.join('public/blient/index.html'), layout: false, status: status
  139. end
  140. def redirect_to(url, opts = {})
  141. return super unless url.is_a? String # GK: for now this override only covers cases where a string has been passed in, so it does not cover cases of a Hash or a Record being passed in
  142. host = URI(url).host
  143. if ENV['USE_VUE'] && Rails.env.development? && host == "localhost"
  144. path = URI(url).path
  145. query = URI(url).query ? "?#{URI(url).query}" : ""
  146. super "http://localhost:8080#{path}#{query}"
  147. else
  148. super
  149. end
  150. end
  151. def is_old_browser?
  152. browser.ie? || (browser.safari? && browser.version.to_i < 12)
  153. end
  154. end

app/controllers/authenticate_by_unsubscribe_token_controller.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. 1 class AuthenticateByUnsubscribeTokenController < ApplicationController
  2. 1 before_action :authenticate_user_by_unsubscribe_token_or_fallback
  3. 1 private
  4. 1 def user
  5. 13 @restricted_user || current_user
  6. end
  7. 1 def authenticate_user_by_unsubscribe_token_or_fallback
  8. 7 unless (params[:unsubscribe_token].present? and @restricted_user = User.find_by_unsubscribe_token(params[:unsubscribe_token]))
  9. authenticate_user!
  10. end
  11. end
  12. end

app/controllers/dev/base_controller.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. class Dev::BaseController < ApplicationController
  2. before_action :ensure_not_production
  3. def index
  4. @routes = self.class.action_methods.select do |action|
  5. /^(test_|setup_|view_)/.match action
  6. end
  7. render 'dev/main/index', layout: false
  8. end
  9. def import_test_data
  10. GroupExportService.import('tmp/test.json')
  11. sign_in User.first
  12. redirect_to Group.order('memberships_count desc').first
  13. end
  14. def last_email(to: nil)
  15. @email = if to.present?
  16. ActionMailer::Base.deliveries.filter { |email| Array(email.to).include?(to.email) }
  17. else
  18. ActionMailer::Base.deliveries
  19. end.last
  20. render template: 'dev/main/last_email', layout: false
  21. end
  22. private
  23. def ensure_not_production
  24. raise "Development and testing only" if Rails.env.production?
  25. end
  26. end

app/controllers/dev/discussions_controller.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. class Dev::DiscussionsController < Dev::BaseController
  2. include Dev::FakeDataHelper
  3. def test_none_read
  4. discussion = create_discussion_with_nested_comments
  5. sign_in discussion.author
  6. redirect_to discussion_url(discussion)
  7. end
  8. def test_some_read
  9. discussion = create_discussion_with_nested_comments
  10. EventService.repair_thread(discussion.id)
  11. discussion.author.experienced!('betaFeatures')
  12. sign_in discussion.author
  13. read_ids = discussion.items.order(sequence_id: :asc).limit(5).pluck(:sequence_id)
  14. DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
  15. redirect_to discussion_url(discussion)
  16. end
  17. def test_most_read
  18. discussion = create_discussion_with_nested_comments
  19. sign_in discussion.author
  20. read_ids = discussion.items.order(sequence_id: :asc).limit(5).pluck(:sequence_id)
  21. DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
  22. redirect_to discussion_url(discussion)
  23. end
  24. def test_all_read
  25. discussion = create_discussion_with_nested_comments
  26. sign_in discussion.author
  27. read_ids = discussion.items.order(sequence_id: :asc).pluck(:sequence_id)
  28. DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
  29. redirect_to discussion_url(discussion)
  30. end
  31. def test_sampled_comments
  32. discussion = create_discussion_with_sampled_comments
  33. sign_in discussion.author
  34. redirect_to discussion_url(discussion)
  35. end
  36. end

app/controllers/dev/nightwatch_controller.rb

0.0% lines covered

34 relevant lines. 0 lines covered and 34 lines missed.
    
  1. class Dev::NightwatchController < Dev::BaseController
  2. include Dev::NintiesMoviesHelper
  3. include PrettyUrlHelper
  4. include Dev::Scenarios::Util
  5. include Dev::Scenarios::Auth
  6. include Dev::Scenarios::Dashboard
  7. include Dev::Scenarios::Discussion
  8. include Dev::Scenarios::EmailSettings
  9. include Dev::Scenarios::Group
  10. include Dev::Scenarios::Inbox
  11. include Dev::Scenarios::JoinGroup
  12. include Dev::Scenarios::MembershipRequest
  13. include Dev::Scenarios::Membership
  14. include Dev::Scenarios::Notification
  15. include Dev::Scenarios::Profile
  16. include Dev::Scenarios::Tags
  17. before_action :redis_flushall, except: [
  18. :last_email,
  19. :use_last_login_token,
  20. :index,
  21. :accept_last_invitation,
  22. ]
  23. before_action :cleanup_database, except: [
  24. :last_email,
  25. :use_last_login_token,
  26. :index,
  27. :accept_last_invitation,
  28. ]
  29. def redis_flushall
  30. CACHE_REDIS_POOL.with do |client|
  31. client.flushall
  32. end
  33. end
  34. end

app/controllers/dev/polls_controller.rb

0.0% lines covered

103 relevant lines. 0 lines covered and 103 lines missed.
    
  1. class Dev::PollsController < Dev::NightwatchController
  2. include Dev::ScenariosHelper
  3. def test_poll_scenario
  4. scenario =send(:"#{params[:scenario]}_scenario", {
  5. poll_type: params[:poll_type],
  6. anonymous: !!params[:anonymous],
  7. hide_results: (params[:hide_results] || :off),
  8. admin: !!params[:admin],
  9. guest: !!params[:guest],
  10. standalone: !!params[:standalone],
  11. wip: !!params[:wip]
  12. })
  13. scenario[:group].add_admin! scenario[:observer]
  14. sign_in(scenario[:observer]) if scenario[:observer].is_a?(User)
  15. if params[:email]
  16. @scenario = scenario
  17. last_email to: scenario[:observer]
  18. else
  19. redirect_to poll_url(scenario[:poll], Hash(scenario[:params]))
  20. end
  21. end
  22. def test_invite_to_poll
  23. admin = saved fake_user
  24. group = saved fake_group
  25. group.add_admin! admin
  26. if params[:guest]
  27. user = saved fake_unverified_user
  28. else
  29. user = saved fake_user
  30. group.add_member! user
  31. end
  32. discussion = fake_discussion(group: group)
  33. DiscussionService.create(discussion: discussion, actor: admin)
  34. # select poll type here
  35. poll = fake_poll(group: group, discussion: discussion, author: admin)
  36. PollService.create(poll: poll, actor: poll.author, params: {notify_recipients: true})
  37. if params[:guest]
  38. PollService.invite(poll: poll, params: {recipient_emails: [user.email], notify_recipients: true}, actor: poll.author)
  39. end
  40. last_email
  41. end
  42. def test_discussion
  43. group = create_group_with_members
  44. sign_in group.admins.first
  45. discussion = saved fake_discussion(group: group, author: group.admins.first)
  46. DiscussionService.create(discussion: discussion, actor: discussion.author)
  47. redirect_to discussion_path(discussion)
  48. end
  49. def test_poll_in_discussion
  50. group = create_group_with_members
  51. sign_in group.admins.first
  52. discussion = saved fake_discussion(group: group, author: group.admins.first)
  53. DiscussionService.create(discussion: discussion, actor: discussion.author)
  54. poll = saved fake_poll(discussion: discussion)
  55. stance = saved fake_stance(poll: poll)
  56. StanceService.create(stance: stance, actor: group.members.last)
  57. redirect_to poll_url(poll)
  58. end
  59. def start_poll
  60. sign_in saved fake_user
  61. redirect_to new_poll_url
  62. end
  63. def test_activity_items
  64. user = fake_user
  65. group = saved fake_group
  66. group.add_admin! user
  67. discussion = saved fake_discussion(group: group)
  68. DiscussionService.create(discussion: discussion, actor: discussion.author)
  69. sign_in user
  70. create_activity_items(discussion: discussion, actor: user)
  71. redirect_to discussion_url(discussion)
  72. end
  73. private
  74. def create_activity_items(discussion: , actor: )
  75. # create poll
  76. options = {poll: %w[apple turnip peach],
  77. count: %w[yes no],
  78. proposal: %w[agree disagree abstain block],
  79. dot_vote: %w[birds bees trees]}
  80. AppConfig.poll_types.keys.each do |poll_type|
  81. poll = Poll.new(poll_type: poll_type,
  82. title: poll_type,
  83. details: 'fine print',
  84. poll_option_names: options[poll_type.to_sym],
  85. discussion: discussion)
  86. PollService.create(poll: poll, actor: actor)
  87. # edit the poll
  88. PollService.update(poll: poll, params: {title: 'choose!'}, actor: actor)
  89. # vote on the poll
  90. stance = Stance.new(poll: poll,
  91. choice: poll.poll_option_names.first,
  92. reason: 'democracy is in my shoes')
  93. StanceService.create(stance: stance, actor: actor)
  94. # close the poll
  95. PollService.close(poll: poll, actor: actor)
  96. # set an outcome
  97. outcome = Outcome.new(poll: poll, statement: 'We all voted')
  98. OutcomeService.create(outcome: outcome, actor: actor)
  99. # create poll
  100. poll = Poll.new(poll_type: poll_type,
  101. title: 'Which one?',
  102. details: 'fine print',
  103. poll_option_names: options[poll_type.to_sym],
  104. discussion: discussion)
  105. PollService.create(poll: poll, actor: actor)
  106. poll.update_attribute(:closing_at, 1.day.ago)
  107. # expire the poll
  108. PollService.expire_lapsed_polls
  109. end
  110. end
  111. end

app/controllers/dev/scenarios/auth.rb

0.0% lines covered

135 relevant lines. 0 lines covered and 135 lines missed.
    
  1. module Dev::Scenarios::Auth
  2. def setup_invitation_email_to_visitor
  3. group = create_group
  4. params = {recipient_emails: ['newuser@example.com'], recipient_message: 'Hey this is the app I told you about. please accept the inviitation!'}
  5. GroupService.invite(group:group, params: params, actor: group.creator)
  6. last_email
  7. end
  8. def setup_invite_user_with_alternative_email
  9. group = create_group
  10. group.update(group_privacy: 'secret')
  11. user = User.create(email: 'existing-user@example.com',
  12. name: 'existing user',
  13. email_verified: true,
  14. password: 'veryeasytoguess123')
  15. GroupService.invite(
  16. group:group,
  17. params: {
  18. recipient_message: "hi, please join our sweet group!",
  19. recipient_emails: ['newuser@example.com']
  20. },
  21. actor: group.creator)
  22. sign_in user if params[:signed_in]
  23. last_email
  24. end
  25. def setup_invite_user_with_correct_email
  26. group = create_group
  27. group.update(group_privacy: 'secret')
  28. user = User.create(email: 'existing-user@example.com',
  29. name: 'existing user',
  30. email_verified: true,
  31. password: 'veryeasytoguess123')
  32. params = {recipient_emails: ['existing-user@example.com'], recipient_message: "hi, please join our sweet group!"}
  33. GroupService.invite(group:group, params: params, actor: group.creator)
  34. sign_in user if params[:signed_in]
  35. last_email
  36. end
  37. def setup_invitation_email_to_user_with_password
  38. group = create_group
  39. another_group = saved fake_group
  40. user = saved fake_user(password: nil, name: 'fake user')
  41. another_group.add_member! user
  42. another_group.add_member! group.creator
  43. user.reload
  44. group.creator.reload
  45. params = {recipient_user_ids: [user.id], recipient_message: "click accept,
  46. please
  47. thanks" }
  48. GroupService.invite(group:group, params: params, actor: group.creator)
  49. last_email
  50. end
  51. def setup_membership_request_email
  52. group = saved fake_group(is_visible_to_public: true, membership_granted_upon: 'approval')
  53. admin = saved fake_user
  54. GroupService.create(group: group, actor: admin)
  55. user = saved fake_user
  56. membership_request = ::MembershipRequest.new(requestor: user, group: group, introduction: "Hey, I'm a shady person who just wants to post spam into your group!")
  57. MembershipRequestService.create(
  58. membership_request: membership_request,
  59. actor: user
  60. )
  61. sign_in admin
  62. last_email
  63. end
  64. def setup_deactivated_user
  65. patrick.update(deactivated_at: 1.day.ago)
  66. redirect_to dashboard_url
  67. end
  68. def setup_login_token
  69. login_token = FactoryBot.create(:login_token, user: patrick)
  70. redirect_to(login_token_url(login_token.token))
  71. end
  72. def setup_login_token_email
  73. login_token = FactoryBot.create(:login_token, user: patrick)
  74. UserMailer.login(patrick.id, login_token.id).deliver
  75. redirect_to('/dev/last_email')
  76. end
  77. def setup_used_login_token
  78. login_token = FactoryBot.create(:login_token, user: patrick, used: true)
  79. redirect_to(login_token_url(login_token.token))
  80. end
  81. def setup_explore_as_visitor
  82. patrick
  83. recent_discussion
  84. redirect_to explore_url
  85. end
  86. def view_closed_group_with_shareable_link
  87. redirect_to join_url(create_group)
  88. end
  89. def view_open_discussion_as_visitor
  90. @group = Group.create!(name: 'Open Dirty Dancing Shoes',
  91. membership_granted_upon: 'request',
  92. group_privacy: 'open')
  93. @group.add_member! patrick
  94. @group.add_admin! jennifer
  95. @discussion = Discussion.new(title: 'I carried a watermelon', private: false, author: jennifer, group: @group)
  96. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  97. redirect_to discussion_url(@discussion)
  98. end
  99. def view_closed_group_as_non_member
  100. sign_in patrick
  101. @group = Group.create!(name: 'Closed Dirty Dancing Shoes',
  102. group_privacy: 'closed',
  103. discussion_privacy_options: 'public_or_private')
  104. @group.add_admin! jennifer
  105. @discussion = Discussion.new(title: "I carried a watermelon", private: false, author: jennifer, group: @group)
  106. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  107. redirect_to group_url(@group)
  108. end
  109. def view_secret_group_as_non_member
  110. patrick.update(is_admin: false)
  111. sign_in patrick
  112. @group = Group.create!(name: 'Secret Dirty Dancing Shoes',
  113. group_privacy: 'secret')
  114. redirect_to group_url(@group)
  115. end
  116. def view_closed_group_as_visitor
  117. @group = Group.create!(name: 'Closed Dirty Dancing Shoes',
  118. membership_granted_upon: 'approval',
  119. group_privacy: 'closed',
  120. discussion_privacy_options: 'public_or_private')
  121. @group.add_member! patrick
  122. @group.add_admin! jennifer
  123. @discussion = @group.discussions.create!(title: 'This thread is private', private: true, author: jennifer)
  124. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  125. @public_discussion = @group.discussions.create!(title: 'This thread is public', private: false, author: jennifer)
  126. DiscussionService.create(discussion: @public_discussion, actor: @public_discussion.author)
  127. redirect_to group_url(@group)
  128. end
  129. def view_secret_group_as_visitor
  130. @group = Group.create!(name: 'Secret Dirty Dancing Shoes',
  131. group_privacy: 'secret')
  132. @group.add_admin! patrick
  133. redirect_to group_url(@group)
  134. end
  135. end

app/controllers/dev/scenarios/dashboard.rb

0.0% lines covered

20 relevant lines. 0 lines covered and 20 lines missed.
    
  1. module Dev::Scenarios::Dashboard
  2. include Dev::DashboardHelper
  3. def setup_dashboard
  4. sign_in patrick
  5. pinned_discussion
  6. poll_discussion
  7. recent_discussion
  8. redirect_to dashboard_url
  9. end
  10. def setup_dashboard_with_one_thread
  11. sign_in patrick
  12. recent_discussion
  13. redirect_to dashboard_url
  14. end
  15. def setup_dashboard_as_visitor
  16. patrick; jennifer
  17. recent_discussion
  18. redirect_to dashboard_url
  19. end
  20. end

app/controllers/dev/scenarios/discussion.rb

0.0% lines covered

187 relevant lines. 0 lines covered and 187 lines missed.
    
  1. module Dev::Scenarios::Discussion
  2. def setup_discussion
  3. create_discussion
  4. sign_in patrick
  5. redirect_to discussion_url(create_discussion)
  6. end
  7. def setup_multiple_discussions
  8. sign_in patrick
  9. create_discussion
  10. create_public_discussion
  11. redirect_to discussion_url(create_discussion)
  12. end
  13. def setup_discussion_as_guest
  14. group = FactoryBot.create :group, group_privacy: 'secret'
  15. discussion = FactoryBot.build :discussion, group: group, title: "Dirty Dancing Shoes"
  16. DiscussionService.create(discussion: discussion, actor: discussion.group.creator)
  17. discussion.add_guest!(jennifer, discussion.author)
  18. sign_in jennifer
  19. redirect_to discussion_url(discussion)
  20. end
  21. def setup_forkable_discussion
  22. create_discussion
  23. create_another_discussion
  24. sign_in patrick
  25. CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally on topic!"), actor: jennifer)
  26. event = CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally **off** topic!"), actor: jennifer)
  27. CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is a reply to the off-topic thing!", parent: event.eventable), actor: emilio)
  28. CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is also off-topic"), actor: emilio)
  29. CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally back on topic!"), actor: patrick)
  30. redirect_to discussion_url(create_discussion)
  31. end
  32. def setup_thread_catch_up
  33. jennifer.update(email_catch_up_day: 7)
  34. CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "first comment"), actor: patrick)
  35. event = CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "removed comment"), actor: patrick)
  36. CommentService.discard(comment: event.eventable, actor: event.user)
  37. DiscussionService.update(discussion: create_discussion,
  38. params: {recipient_message: 'this is an edit message'},
  39. actor: patrick)
  40. poll = fake_poll
  41. PollService.create(poll: poll, actor: patrick)
  42. create_fake_stances(poll: poll)
  43. PollService.update(poll: poll, actor: patrick, params: {recipient_message: 'updated the poll here <br> newline'})
  44. DiscussionService.close(discussion: create_discussion, actor: patrick)
  45. UserMailer.catch_up(jennifer.id, 1.hour.ago).deliver_now
  46. last_email
  47. end
  48. def setup_unread_discussion
  49. read = Comment.new(discussion: create_discussion, body: "Here is some read content")
  50. unread = Comment.new(discussion: create_discussion, body: "Here is some unread content")
  51. another_unread = Comment.new(discussion: create_discussion, body: "Here is some more unread content")
  52. sign_in patrick
  53. CommentService.create(comment: read, actor: patrick)
  54. CommentService.create(comment: unread, actor: jennifer)
  55. CommentService.create(comment: another_unread, actor: jennifer)
  56. redirect_to discussion_url(create_discussion)
  57. end
  58. def setup_discussion_for_jennifer
  59. sign_in jennifer
  60. redirect_to discussion_url(create_discussion)
  61. end
  62. def setup_open_and_closed_discussions
  63. create_discussion
  64. create_closed_discussion
  65. sign_in patrick
  66. patrick.update(experiences: { closingThread: true })
  67. redirect_to group_url(create_group)
  68. end
  69. def setup_pages_of_closed_discussions
  70. @group = saved(fake_group)
  71. @group.add_admin!(patrick)
  72. sign_in patrick
  73. 60.times do
  74. saved(fake_discussion(group: @group, closed_at: 5.days.ago))
  75. end
  76. redirect_to group_url(@group)
  77. end
  78. def setup_comment_with_versions
  79. comment = Comment.new(discussion: create_discussion, body: "What star sign are you?")
  80. CommentService.create(comment: comment, actor: jennifer)
  81. comment.update(body: "What moon sign are you?")
  82. comment.update_versions_count
  83. sign_in patrick
  84. redirect_to discussion_url(create_discussion)
  85. end
  86. def setup_discussion_with_versions
  87. create_discussion
  88. create_discussion.update(title: "What moon sign are you?")
  89. create_discussion.update_versions_count
  90. sign_in patrick
  91. redirect_to discussion_url(create_discussion)
  92. end
  93. # discussion mailer emails
  94. def setup_discussion_mailer_discussion_created_email
  95. sign_in jennifer
  96. @group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
  97. @group.add_admin! patrick
  98. @group.add_member! jennifer
  99. discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
  100. discussion.files.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'strongbad.png')),
  101. filename: 'strongbad.png',
  102. content_type: 'image/jpeg')
  103. DiscussionService.create(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id]})
  104. last_email
  105. end
  106. def setup_discussion_mailer_discussion_edited_email
  107. sign_in jennifer
  108. @group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
  109. @group.add_admin! patrick
  110. @group.add_member! jennifer
  111. discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
  112. DiscussionService.create(discussion: discussion, actor: patrick)
  113. DiscussionService.update(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id], recipient_message: 'change message & ampersand <yo>! &nbsp;'})
  114. last_email
  115. end
  116. def setup_discussion_mailer_discussion_announced_email
  117. sign_in jennifer
  118. @group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
  119. @group.add_admin! patrick
  120. @group.add_member! jennifer
  121. discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
  122. event = DiscussionService.create(discussion: discussion, actor: patrick)
  123. DiscussionService.invite(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id]})
  124. last_email
  125. end
  126. def setup_discussion_mailer_invitation_created_email
  127. group = FactoryBot.create(:group, name: "Dirty Dancing Shoes", creator: patrick)
  128. group.add_admin! patrick
  129. discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: group)
  130. event = DiscussionService.create(discussion: discussion, actor: patrick)
  131. comment = FactoryBot.build(:comment, discussion: discussion)
  132. CommentService.create(comment: comment, actor: patrick)
  133. DiscussionService.invite(discussion: discussion, actor: patrick, params: {recipient_emails: 'jen@example.com'})
  134. last_email
  135. end
  136. def setup_discussion_mailer_new_comment_email
  137. @group = Group.create!(name: 'Dirty Dancing Shoes')
  138. @group.add_admin!(patrick).set_volume!(:loud)
  139. @group.add_member! jennifer
  140. @discussion = Discussion.new(title: 'What star sign are you?',
  141. group: @group,
  142. description: "Wow, what a __great__ day.",
  143. author: jennifer)
  144. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  145. @comment = Comment.new(author: jennifer, body: "hello _patrick_.", discussion: @discussion)
  146. CommentService.create(comment: @comment, actor: jennifer)
  147. last_email
  148. end
  149. def setup_discussion_mailer_comment_replied_to_email
  150. @group = Group.create!(name: 'Dirty Dancing Shoes')
  151. @group.add_admin!(patrick)
  152. @group.add_member! jennifer
  153. @discussion = Discussion.new(title: 'What star sign are you?',
  154. group: @group,
  155. description: "Wow, what a __great__ day.",
  156. author: jennifer)
  157. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  158. @comment = Comment.new(body: "hello _patrick.", discussion: @discussion)
  159. CommentService.create(comment: @comment, actor: jennifer)
  160. @reply_comment = Comment.new(body: "why, hello there @#{jennifer.username}", parent: @comment, discussion: @discussion)
  161. CommentService.create(comment: @reply_comment, actor: patrick)
  162. last_email
  163. end
  164. def setup_discussion_mailer_user_mentioned_email
  165. @group = saved fake_group
  166. GroupService.create(group: @group, actor: patrick)
  167. @group.add_member! jennifer
  168. @discussion = fake_discussion(group: @group, description: "hey @#{patrick.username} wanna dance?")
  169. DiscussionService.create(discussion: @discussion, actor: jennifer)
  170. last_email
  171. end
  172. def setup_task_reminder_email
  173. @group = Group.create!(name: 'Dirty Dancing Shoes')
  174. @group.add_admin!(patrick)
  175. jennifer.update(time_zone: "Pacific/Auckland")
  176. @group.add_member! jennifer
  177. datestr = "2021-06-16"
  178. @discussion = Discussion.new(title: 'time to do your chores!',
  179. description_format: 'html',
  180. group: @group,
  181. description: "<li data-uid='123' data-type='taskItem' data-due-on='#{datestr}' data-remind='1'>this is a task for <span data-mention-id='#{jennifer.username}'>#{jennifer.name}</span></li>",
  182. author: jennifer)
  183. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  184. expected_remind_at = "{datestr} 06:00".in_time_zone("Pacific/Auckland") - 1.day
  185. TaskService.send_task_reminders(expected_remind_at)
  186. last_email
  187. end
  188. end

app/controllers/dev/scenarios/email_settings.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. module Dev::Scenarios::EmailSettings
  2. def email_settings_as_logged_in_user
  3. create_group
  4. sign_in patrick
  5. redirect_to email_preferences_url(unsubscribe_token: patrick.unsubscribe_token)
  6. end
  7. def email_settings_as_restricted_user
  8. create_group
  9. redirect_to email_preferences_url(unsubscribe_token: patrick.unsubscribe_token)
  10. end
  11. end

app/controllers/dev/scenarios/group.rb

0.0% lines covered

203 relevant lines. 0 lines covered and 203 lines missed.
    
  1. module Dev::Scenarios::Group
  2. def setup_group_super_admin
  3. patrick.update(is_admin: true)
  4. sign_in patrick
  5. create_group.add_member! emilio
  6. redirect_to group_url(create_group)
  7. end
  8. def setup_group
  9. sign_in patrick
  10. create_group.add_member! emilio
  11. redirect_to group_url(create_group)
  12. end
  13. def setup_group_with_received_email
  14. sign_in patrick
  15. create_group.add_member! emilio
  16. 5.times do
  17. name = Faker::Name.name
  18. email = ReceivedEmail.create(
  19. body_html: "<html><body>hello everyone.</body></html>",
  20. dkim_valid: true,
  21. spf_valid: true,
  22. headers: {
  23. from: "\"#{name}\" <#{Faker::Internet.email(name: name)}>",
  24. to: create_group.handle + "@#{ENV['REPLY_HOSTNAME']}",
  25. subject: Faker::TvShows::TheFreshPrinceOfBelAir.quote
  26. }
  27. )
  28. end
  29. ReceivedEmailService.route_all
  30. redirect_to group_emails_url(create_group)
  31. end
  32. def setup_group_with_max_members
  33. sign_in patrick
  34. create_group.subscription.update(max_members: 4)
  35. redirect_to group_memberships_url(create_group)
  36. end
  37. def setup_trial_group_with_received_email
  38. sign_in patrick
  39. create_group.subscription.update(plan: 'trial')
  40. create_group.add_member! emilio
  41. 5.times do
  42. name = Faker::Name.name
  43. email = ReceivedEmail.create(
  44. body_html: "<html><body>hello everyone.</body></html>",
  45. dkim_valid: true,
  46. spf_valid: true,
  47. headers: {
  48. from: "\"#{name}\" <#{Faker::Internet.email(name: name)}>",
  49. to: create_group.handle + "@#{ENV['REPLY_HOSTNAME']}",
  50. subject: Faker::TvShows::TheFreshPrinceOfBelAir.quote
  51. }
  52. )
  53. end
  54. ReceivedEmailService.route_all
  55. redirect_to group_emails_url(create_group)
  56. end
  57. def setup_user_no_group
  58. sign_in patrick
  59. redirect_to dashboard_url
  60. end
  61. def setup_group_with_discussion
  62. sign_in patrick
  63. create_group.add_member! emilio
  64. create_discussion
  65. redirect_to group_url(create_group)
  66. end
  67. def setup_group_with_handle
  68. sign_in patrick
  69. group = create_group
  70. group.update(name: 'Ghostbusters', handle: 'ghostbusters')
  71. redirect_to group_url(group)
  72. end
  73. def setup_group_with_pending_invitations
  74. sign_in patrick
  75. create_group
  76. other_invite = FactoryBot.create(:user, name: nil, email: "hidden@test.com")
  77. my_invite = FactoryBot.create(:user, name: nil, email: "shown@test.com")
  78. FactoryBot.create :membership, group: create_group, accepted_at: nil, inviter: jennifer, user: other_invite
  79. FactoryBot.create :membership, group: create_group, accepted_at: nil, inviter: patrick, user: my_invite
  80. redirect_to group_url(create_group)
  81. end
  82. def visit_group_as_subgroup_member
  83. sign_in jennifer
  84. create_subgroup.add_member! jennifer
  85. another_create_subgroup.add_member! jennifer
  86. redirect_to group_url(create_another_group)
  87. end
  88. def setup_group_with_subgroups
  89. sign_in jennifer
  90. create_another_group.add_member! jennifer
  91. create_subgroup.add_member! jennifer
  92. another_create_subgroup
  93. redirect_to group_url(create_another_group)
  94. end
  95. def setup_group_with_subgroups_as_admin
  96. sign_in jennifer
  97. create_another_group.add_admin! jennifer
  98. create_subgroup.add_member! jennifer
  99. create_subgroup.add_member! fake_user name: 'only in subgroup'
  100. another_create_subgroup
  101. redirect_to group_url(create_subgroup)
  102. end
  103. def setup_subgroup_with_parent_member_visibility
  104. sign_in patrick
  105. @group = Group.create!(name: 'Closed Dirty Dancing Shoes',
  106. group_privacy: 'closed')
  107. @group.add_admin! jennifer
  108. @group.add_member! jennifer
  109. @group.add_member! patrick
  110. @subgroup = Group.create!(name: 'Johnny Utah',
  111. parent: @group,
  112. discussion_privacy_options: 'public_or_private',
  113. parent_members_can_see_discussions: true,
  114. group_privacy: 'closed', creator: jennifer)
  115. discussion = FactoryBot.create :discussion, group: @subgroup, title: "Vaya con dios", private: true, author: jennifer
  116. DiscussionService.create(discussion: discussion, actor: discussion.author)
  117. redirect_to group_url(@subgroup)
  118. end
  119. def setup_group_with_subgroups_as_admin_landing_in_other_subgroup
  120. sign_in jennifer
  121. create_another_group.add_admin! jennifer
  122. create_subgroup.add_member! jennifer
  123. another_create_subgroup
  124. redirect_to group_url(another_create_subgroup)
  125. end
  126. def setup_open_group
  127. @group = Group.create!(name: 'Open Dirty Dancing Shoes',
  128. group_privacy: 'open')
  129. @group.add_admin! patrick
  130. @group.add_member! jennifer
  131. membership = Membership.find_by(user: patrick, group: @group)
  132. sign_in patrick
  133. redirect_to group_url(create_group)
  134. end
  135. def setup_closed_group
  136. @group = Group.create!(name: 'Closed Dirty Dancing Shoes', group_privacy: 'closed')
  137. @group.add_admin! patrick
  138. @group.add_member! jennifer
  139. membership = Membership.find_by(user: patrick, group: @group)
  140. sign_in patrick
  141. redirect_to group_url(create_group)
  142. end
  143. def setup_secret_group
  144. @group = Group.create!(name: 'Secret Dirty Dancing Shoes', handle: 'secret-shoes', group_privacy: 'secret')
  145. @group.add_admin! patrick
  146. @group.add_member! jennifer
  147. membership = Membership.find_by(user: patrick, group: @group)
  148. sign_in patrick
  149. redirect_to group_url(create_group)
  150. end
  151. def setup_group_with_multiple_coordinators
  152. create_group.add_admin! emilio
  153. sign_in patrick
  154. redirect_to group_url(create_group)
  155. end
  156. def setup_group_with_no_coordinators
  157. create_group
  158. @group.admin_memberships.each{|m| m.update(admin: false)}
  159. sign_in patrick
  160. redirect_to group_url(create_group)
  161. end
  162. def setup_group_with_restrictive_settings
  163. sign_in max
  164. create_stance
  165. create_discussion
  166. create_group.update(
  167. members_can_add_members: false,
  168. members_can_edit_discussions: false,
  169. members_can_edit_comments: false,
  170. members_can_raise_motions: false,
  171. members_can_start_discussions: false,
  172. members_can_create_subgroups: false
  173. )
  174. create_group.add_member! max
  175. redirect_to group_url create_group
  176. end
  177. def view_open_group_as_non_member
  178. sign_in patrick
  179. @group = Group.create!(name: 'Open Dirty Dancing Shoes', membership_granted_upon: 'request', group_privacy: 'open')
  180. @group.add_admin! jennifer
  181. @discussion = Discussion.new(title: "I carried a watermelon", private: false, author: jennifer, group: @group)
  182. DiscussionService.create(discussion: @discussion, actor: jennifer)
  183. CommentService.create(comment: Comment.new(body: "It was real seedy", discussion: @discussion), actor: jennifer)
  184. redirect_to group_url(create_group)
  185. end
  186. def view_open_group_as_visitor
  187. @group = Group.create!(name: 'Open Dirty Dancing Shoes',
  188. membership_granted_upon: 'request',
  189. group_privacy: 'open')
  190. @group.add_admin! jennifer
  191. @discussion = Discussion.new(title: 'I carried a watermelon', private: false, author: jennifer, group: @group)
  192. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  193. redirect_to group_url(@group)
  194. end
  195. def setup_start_thread_form_from_url
  196. sign_in patrick
  197. redirect_to "/d/new/?group_id=#{create_group.id}&title=testing title&type=thread"
  198. end
  199. def setup_start_poll_form_from_url
  200. sign_in patrick
  201. redirect_to "/p/new/count?group_id=#{create_group.id}&title=testing title"
  202. end
  203. end

app/controllers/dev/scenarios/inbox.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module Dev::Scenarios::Inbox
  2. def setup_inbox
  3. sign_in patrick
  4. recent_discussion group: create_another_group
  5. old_discussion; pinned_discussion
  6. redirect_to inbox_url
  7. end
  8. end

app/controllers/dev/scenarios/join_group.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. module Dev::Scenarios::JoinGroup
  2. def setup_public_group_to_join_upon_request
  3. sign_in jennifer
  4. create_another_group.update(group_privacy: 'open')
  5. create_another_group.update(membership_granted_upon: 'request')
  6. create_public_discussion
  7. redirect_to group_url(create_another_group)
  8. end
  9. def setup_closed_group_to_join
  10. sign_in jennifer
  11. create_another_group
  12. create_public_discussion
  13. private_create_discussion
  14. create_subgroup
  15. redirect_to group_url(create_another_group)
  16. end
  17. end

app/controllers/dev/scenarios/membership.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. module Dev::Scenarios::Membership
  2. def setup_group_as_member
  3. create_group.update_admin_memberships_count
  4. sign_in jennifer
  5. redirect_to group_url(create_group)
  6. end
  7. def setup_membership_with_title
  8. sign_in patrick
  9. create_group.memberships.find_by(user: patrick).update(title: "Suzerain!")
  10. redirect_to group_url(create_group)
  11. end
  12. end

app/controllers/dev/scenarios/membership_request.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. module Dev::Scenarios::MembershipRequest
  2. def setup_membership_requests
  3. sign_in patrick
  4. create_group
  5. 3.times do
  6. request = MembershipRequest.new(group: create_group, introduction: "I'd like to make decisions with y'all")
  7. MembershipRequestService.create(membership_request: request, actor: saved(fake_user))
  8. end
  9. redirect_to group_url(create_group)
  10. end
  11. end

app/controllers/dev/scenarios/notification.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. module Dev::Scenarios::Notification
  2. def setup_all_activity_items
  3. create_discussion
  4. sign_in patrick
  5. create_all_activity_items
  6. redirect_to discussion_url(create_discussion)
  7. end
  8. def setup_all_notifications
  9. sign_in patrick
  10. create_all_notifications
  11. redirect_to discussion_url(create_discussion)
  12. end
  13. end

app/controllers/dev/scenarios/profile.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. module Dev::Scenarios::Profile
  2. def setup_restricted_profile
  3. sign_in patrick
  4. create_group = Group.create!(name: 'Secret Dirty Dancing Shoes',
  5. group_privacy: 'secret')
  6. create_group.add_member!(jennifer)
  7. redirect_to "/u/#{jennifer.username}"
  8. end
  9. def setup_profile_with_group_visible_to_members
  10. sign_in patrick
  11. create_group = Group.create!(name: 'Secret Dirty Dancing Shoes',
  12. group_privacy: 'secret')
  13. create_group.add_admin!(patrick)
  14. create_group.add_member!(jennifer)
  15. redirect_to "/u/#{jennifer.username}"
  16. end
  17. def setup_deactivated_user
  18. patrick.update(deactivated_at: 1.day.ago)
  19. redirect_to "/dashboard"
  20. end
  21. def setup_user_reactivation_email
  22. patrick.update(deactivated_at: 1.day.ago)
  23. UserService.reactivate(patrick.id)
  24. last_email
  25. end
  26. end

app/controllers/dev/scenarios/tags.rb

0.0% lines covered

31 relevant lines. 0 lines covered and 31 lines missed.
    
  1. module Dev::Scenarios::Tags
  2. def setup_discussion_with_tag
  3. tag = Tag.create(name: "Tag Name", color: "#cccccc", group: create_discussion.group)
  4. sign_in patrick
  5. redirect_to discussion_url(create_discussion)
  6. end
  7. def setup_inbox_with_tag
  8. tag = Tag.create(name: "Tag Name", color: "#cccccc", group: create_discussion.group)
  9. discussion_tag = DiscussionTag.create(discussion: create_discussion, tag: tag)
  10. sign_in patrick
  11. redirect_to inbox_url
  12. end
  13. def view_discussion_as_visitor_with_tags
  14. group = Group.create!(name: 'Open Dirty Dancing Shoes', group_privacy: 'open')
  15. group.add_admin! patrick
  16. discussion = group.discussions.create!(title: 'This thread is public', private: false, author: patrick)
  17. DiscussionService.create(discussion: discussion, actor: discussion.author)
  18. tag = group.tags.create(name: "Tag Name", color: "#cccccc")
  19. discussion_tag = discussion.discussion_tags.create(tag: tag)
  20. redirect_to discussion_url(discussion)
  21. end
  22. def visit_tags_page
  23. group = Group.create!(name: 'Open Dirty Dancing Shoes', group_privacy: 'open')
  24. group.add_admin! patrick
  25. discussion = group.discussions.create!(title: 'This thread is public', private: false, author: patrick)
  26. DiscussionService.create(discussion: discussion, actor: discussion.author)
  27. tag = group.tags.create(name: "Tag Name", color: "#cccccc")
  28. discussion_tag = discussion.discussion_tags.create(tag: tag)
  29. redirect_to "/g/#{group.key}/tags"
  30. end
  31. end

app/controllers/dev/scenarios/util.rb

0.0% lines covered

29 relevant lines. 0 lines covered and 29 lines missed.
    
  1. module Dev::Scenarios::Util
  2. def accept_last_invitation
  3. membership = Membership.pending.last
  4. MembershipService.redeem(membership: invitation, actor: max)
  5. redirect_to(group_url(membership.group))
  6. end
  7. def use_last_login_token
  8. redirect_to(login_token_url(::LoginToken.last.token))
  9. end
  10. private
  11. def cleanup_database
  12. reset_session
  13. ::User.delete_all
  14. ::Group.delete_all
  15. ::Membership.delete_all
  16. ::Poll.delete_all
  17. ::Outcome.delete_all
  18. ::Event.delete_all
  19. ::Discussion.delete_all
  20. ::Stance.delete_all
  21. ::StanceChoice.delete_all
  22. ::PollOption.delete_all
  23. ::Task.delete_all
  24. ::DiscussionReader.delete_all
  25. ::DiscussionTemplate.delete_all
  26. ::PollTemplate.delete_all
  27. ::ActionMailer::Base.deliveries = []
  28. end
  29. end

app/controllers/direct_uploads_controller.rb

0.0% lines covered

24 relevant lines. 0 lines covered and 24 lines missed.
    
  1. class DirectUploadsController < ActiveStorage::DirectUploadsController
  2. protect_from_forgery with: :exception
  3. skip_before_action :verify_authenticity_token
  4. private
  5. def direct_upload_json(blob)
  6. json = blob.as_json(root: false, methods: :signed_id).merge(
  7. direct_upload: {
  8. url: blob.service_url_for_direct_upload,
  9. headers: blob.service_headers_for_direct_upload
  10. })
  11. json.merge!(download_url:
  12. Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true)
  13. )
  14. if blob.representable?
  15. json.merge!(preview_url:
  16. Rails.application.routes.url_helpers.rails_representation_path(
  17. blob.representation(HasRichText::PREVIEW_OPTIONS),
  18. only_path: true
  19. )
  20. )
  21. end
  22. json
  23. end
  24. end

app/controllers/discussions_controller.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class DiscussionsController < ApplicationController
  2. end

app/controllers/email_actions_controller.rb

87.5% lines covered

32 relevant lines. 28 lines covered and 4 lines missed.
    
  1. 1 class EmailActionsController < AuthenticateByUnsubscribeTokenController
  2. 1 def unfollow_discussion
  3. 2 discussion_reader = DiscussionReader.for(discussion: discussion, user: user)
  4. 2 if ['normal', 'quiet'].include?(params[:new_volume])
  5. discussion_reader.set_volume!(params[:new_volume].to_sym)
  6. else
  7. 2 if discussion_reader.volume_is_loud?
  8. 1 discussion_reader.set_volume! :normal
  9. else
  10. 1 discussion_reader.set_volume! :quiet
  11. end
  12. end
  13. 2 redirect_to root_path, notice: t(:"email_actions.unfollowed_discussion", thread_title: discussion.title)
  14. end
  15. 1 def mark_discussion_as_read
  16. 3 GenericWorker.perform_async(
  17. 'DiscussionService',
  18. 'mark_as_read_simple_params',
  19. discussion.id,
  20. event.sequence_id || [],
  21. user.id,
  22. )
  23. 2 event.notifications.where(user: user).update_all(viewed: true)
  24. 2 respond_with_pixel
  25. rescue ActiveRecord::RecordNotFound
  26. 1 respond_with_pixel
  27. end
  28. 1 def mark_notification_as_read
  29. 1 Notification.find_by!(id: params[:id], user_id: user.id).update(viewed: true)
  30. 1 respond_with_pixel
  31. rescue ActiveRecord::RecordNotFound
  32. respond_with_pixel
  33. end
  34. 1 def mark_summary_email_as_read
  35. 1 GenericWorker.perform_async('DiscussionService', 'mark_summary_email_as_read', user.id, params[:time_start].to_i, params[:time_finish].to_i)
  36. 1 respond_to do |format|
  37. 1 format.html {
  38. flash[:notice] = I18n.t "email.catch_up.marked_as_read_success"
  39. redirect_to root_path
  40. }
  41. 2 format.gif { respond_with_pixel }
  42. end
  43. end
  44. 1 private
  45. 1 def respond_with_pixel
  46. 5 send_file Rails.root.join('app','assets','images', 'empty.gif'), type: 'image/gif', disposition: 'inline'
  47. end
  48. 1 def discussion
  49. 7 @discussion ||= user.discussions.find(params[:discussion_id])
  50. end
  51. 1 def event
  52. 4 @event ||= Event.find params[:event_id]
  53. end
  54. end

app/controllers/groups_controller.rb

82.35% lines covered

17 relevant lines. 14 lines covered and 3 lines missed.
    
  1. 1 class GroupsController < ApplicationController
  2. 1 def index
  3. 1 @groups = Queries::ExploreGroups.new.search_for(params[:q]).order('groups.memberships_count DESC')
  4. 1 @total = @groups.count
  5. 1 limit = params.fetch(:limit, 50)
  6. 1 if @total < limit
  7. 1 @pages = 1
  8. else
  9. if @total % limit > 0
  10. @pages = @total / limit + 1
  11. else
  12. @pages = @total / limit
  13. end
  14. end
  15. 1 @page = params.fetch(:page, 1).to_i.clamp(1, @pages)
  16. 1 @offset = @page == 1 ? 0 : ((@page - 1) * limit)
  17. 1 @groups = @groups.limit(limit).offset(@offset)
  18. end
  19. 1 def export
  20. 3 @exporter = GroupExporter.new(load_and_authorize(:group, :export))
  21. 1 respond_to do |format|
  22. 1 format.html
  23. end
  24. end
  25. end

app/controllers/help_controller.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class HelpController < ApplicationController
  2. def markdown
  3. render layout: false
  4. end
  5. def api
  6. render layout: 'basic'
  7. end
  8. def api2
  9. current_user.save if current_user.api_key_changed?
  10. @group_id = params[:group_id] || 123
  11. @api_key = current_user.api_key
  12. render layout: 'basic'
  13. end
  14. def api3
  15. render layout: 'basic'
  16. end
  17. end

app/controllers/identities/base_controller.rb

0.0% lines covered

74 relevant lines. 0 lines covered and 74 lines missed.
    
  1. class Identities::BaseController < ApplicationController
  2. def oauth
  3. session[:back_to] = params[:back_to] || request.referrer
  4. redirect_to oauth_url
  5. end
  6. def create
  7. if identity.save
  8. associate_identity
  9. redirect_to session.delete(:back_to) || dashboard_path
  10. else
  11. respond_with_error message: "Could not connect to #{controller_name}!"
  12. end
  13. end
  14. def destroy
  15. if i = current_user.identities.find_by(identity_type: controller_name)
  16. i.destroy
  17. redirect_to request.referrer || root_path
  18. else
  19. respond_with_error message: "Not connected to #{controller_name}!"
  20. end
  21. end
  22. private
  23. def client
  24. @client ||= "Clients::#{controller_name.classify}".constantize.instance
  25. end
  26. def redirect_uri
  27. send :"#{controller_name}_authorize_url"
  28. end
  29. def identity
  30. @identity ||= identity_class.new(identity_params).tap { |i| complete_identity(i) }
  31. end
  32. def existing_identity
  33. @existing_identity ||= identity_class.with_user.find_by(
  34. identity_type: identity.identity_type,
  35. uid: identity.uid
  36. )
  37. end
  38. def existing_user
  39. @existing_user ||= User.verified.find_by(email: identity.email)
  40. end
  41. def associate_identity
  42. if user = existing_identity&.user || current_user.presence || existing_user
  43. user.associate_with_identity(identity)
  44. sign_in(user)
  45. flash[:notice] = t(:'devise.sessions.signed_in')
  46. else
  47. session[:pending_identity_id] = identity.tap(&:save).id
  48. end
  49. end
  50. # override with differing ways to fetch the access token from the response
  51. def identity_params
  52. { access_token: client.fetch_access_token(params[:code], redirect_uri).json['access_token'] }
  53. end
  54. # override with additional follow-up API calls if they're needed to gather more info
  55. # (such as logo url, user name, etc)
  56. def complete_identity(i)
  57. i.fetch_user_info
  58. end
  59. def identity_class
  60. "Identities::#{controller_name.classify}".constantize
  61. end
  62. def oauth_url
  63. "#{oauth_host}?#{oauth_params.to_query}"
  64. end
  65. def oauth_host
  66. raise NotImplementedError.new
  67. end
  68. def oauth_params
  69. { client.client_key_name => client.key, redirect_uri: redirect_uri, scope: oauth_scope }
  70. end
  71. def oauth_client_id_field
  72. :client_id
  73. end
  74. def oauth_scope
  75. client.scope.join(',')
  76. end
  77. end

app/controllers/identities/facebook_controller.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. class Identities::FacebookController < Identities::BaseController
  2. before_action :allow_facebook_domains, only: :webview
  3. layout false
  4. def verify
  5. render text: params[:"hub.challenge"]
  6. end
  7. def webhook
  8. Clients::Facebook.instance.post_poll_button(recipient_id)
  9. head :ok
  10. end
  11. def webview
  12. end
  13. private
  14. def recipient_id
  15. params.dig(:entry, 0, :messaging, 0, :sender, :id)
  16. end
  17. def allow_facebook_domains
  18. response.headers['X-FRAME-OPTIONS'] = 'ALLOW_FROM *'
  19. end
  20. def complete_identity(identity)
  21. super
  22. identity.fetch_user_avatar
  23. end
  24. def oauth_host
  25. "https://www.facebook.com/v2.8/dialog/oauth"
  26. end
  27. end

app/controllers/identities/google_controller.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class Identities::GoogleController < Identities::BaseController
  2. private
  3. def oauth_url
  4. super.gsub("%2B", "+")
  5. end
  6. def oauth_host
  7. "https://accounts.google.com/o/oauth2/v2/auth"
  8. end
  9. def oauth_params
  10. super.merge(response_type: :code, scope: client.scope.join('+'))
  11. end
  12. end

app/controllers/identities/nextcloud_controller.rb

0.0% lines covered

15 relevant lines. 0 lines covered and 15 lines missed.
    
  1. class Identities::NextcloudController < Identities::BaseController
  2. private
  3. def oauth_host
  4. ENV['NEXTCLOUD_HOST']
  5. end
  6. def oauth_url
  7. "#{oauth_host}#{oauth_authorize_path}?#{oauth_params.to_query}"
  8. end
  9. def oauth_authorize_path
  10. '/index.php/apps/oauth2/authorize'.freeze
  11. end
  12. def oauth_params
  13. { client.client_key_name => client.key, redirect_uri: redirect_uri, response_type: :code }
  14. end
  15. end

app/controllers/identities/oauth_controller.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class Identities::OauthController < Identities::BaseController
  2. private
  3. def oauth_url
  4. "#{oauth_auth_url}?#{oauth_params.to_query}"
  5. end
  6. def oauth_auth_url
  7. ENV.fetch('OAUTH_AUTH_URL')
  8. end
  9. def oauth_params
  10. { client.client_key_name => client.key, redirect_uri: redirect_uri, scope: ENV.fetch('OAUTH_SCOPE'), response_type: :code }
  11. end
  12. end

app/controllers/identities/saml_controller.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. class Identities::SamlController < Identities::BaseController
  2. skip_before_action :verify_authenticity_token
  3. def metadata
  4. meta = OneLogin::RubySaml::Metadata.new
  5. render :xml => meta.generate(sp_settings), :content_type => "application/samlmetadata+xml"
  6. end
  7. private
  8. def identity
  9. @identity ||= identity_class.new.tap do |i|
  10. i.response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], skip_recipient_check: true)
  11. i.response.settings = i.settings
  12. complete_identity(i)
  13. end
  14. end
  15. def oauth_url
  16. OneLogin::RubySaml::Authrequest.new.create(Identities::Saml.new.settings)
  17. end
  18. def identity_params
  19. { response: params[:SAMLResponse] }
  20. end
  21. def sp_settings
  22. # this is just for testing purposes.
  23. # should retrieve SAML-settings based on subdomain, IP-address, NameID or similar
  24. settings = OneLogin::RubySaml::Settings.new
  25. # When disabled, saml validation errors will raise an exception.
  26. settings.soft = true
  27. #SP section
  28. settings.issuer = saml_metadata_url
  29. settings.assertion_consumer_service_url = saml_oauth_callback_url
  30. settings.assertion_consumer_logout_service_url = saml_unauthorize_url
  31. # onelogin_app_id = "<onelogin-app-id>"
  32. #
  33. # # IdP section
  34. # settings.idp_entity_id = "https://app.onelogin.com/saml/metadata/#{onelogin_app_id}"
  35. # settings.idp_sso_target_url = "https://app.onelogin.com/trust/saml2/http-redirect/sso/#{onelogin_app_id}"
  36. # settings.idp_slo_target_url = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{onelogin_app_id}"
  37. # settings.idp_cert = ""
  38. # or settings.idp_cert_fingerprint = ""
  39. # settings.idp_cert_fingerprint_algorithm = XMLSecurity::Document::SHA1
  40. settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  41. # Security section
  42. settings.security[:authn_requests_signed] = false
  43. settings.security[:logout_requests_signed] = false
  44. settings.security[:logout_responses_signed] = false
  45. settings.security[:metadata_signed] = false
  46. settings.security[:digest_method] = XMLSecurity::Document::SHA1
  47. settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
  48. settings
  49. end
  50. end

app/controllers/login_tokens_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class LoginTokensController < ApplicationController
  2. 1 def show
  3. 2 session[:pending_login_token] = login_token.token
  4. 2 redirect_to login_token.redirect || dashboard_path
  5. end
  6. 1 private
  7. 1 def login_token
  8. 4 @login_token ||= LoginToken.find_by!(token: params[:token])
  9. end
  10. end

app/controllers/manifest_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class ManifestController < ApplicationController
  2. 1 respond_to :json
  3. 1 ICON_SIZES = %w(32 48 128 144 192 512).freeze
  4. 1 def show
  5. 1 render json: {
  6. name: AppConfig.theme[:site_name],
  7. short_name: AppConfig.theme[:site_name],
  8. display: 'standalone',
  9. orientation: 'portrait',
  10. start_url: '/dashboard',
  11. background_color: AppConfig.theme[:primary_color],
  12. theme_color: AppConfig.theme[:text_on_primary_color],
  13. 6 icons: ICON_SIZES.map { |size| icon_for(size) }
  14. }
  15. end
  16. 1 private
  17. 1 def icon_for(size)
  18. {
  19. 6 src: [root_url.chomp('/'), AppConfig.theme[:"icon#{size}_src"]].join(''),
  20. sizes: "#{size}x#{size}",
  21. type: "image/png"
  22. }
  23. end
  24. end

app/controllers/memberships_controller.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class MembershipsController < ApplicationController
  2. 1 include PrettyUrlHelper
  3. 1 def join
  4. 1 group = Group.published.find_by!(token: params.require(:token))
  5. 1 session[:pending_group_token] = group.token
  6. 1 redirect_to back_to_url || polymorphic_url(group)
  7. end
  8. 1 def show
  9. 8 session[:pending_membership_token] = membership.token
  10. 6 redirect_to back_to_url || polymorphic_url(Group.find_by(id: membership.group_id))
  11. rescue ActiveRecord::RecordNotFound
  12. 2 redirect_to join_url(Group.find_by!(token: params[:token]))
  13. end
  14. 1 def consume
  15. 4 head :ok
  16. end
  17. 1 private
  18. 1 def membership
  19. 14 @membership ||= Membership.find_by!(token: params[:token])
  20. end
  21. 1 def back_to_url
  22. 7 @back_to_url ||= begin
  23. 7 url = URI.decode_www_form_component params[:back_to].to_s
  24. 7 url if url.match(/^http[s]?:\/\/#{ENV['CANONICAL_HOST']}/)
  25. end
  26. end
  27. end

app/controllers/merge_users_controller.rb

64.71% lines covered

17 relevant lines. 11 lines covered and 6 lines missed.
    
  1. 1 class MergeUsersController < ApplicationController
  2. 1 layout 'basic'
  3. 1 def confirm
  4. @source_user = User.active.find_by!(id: params[:source_id])
  5. @target_user = User.active.find_by!(id: params[:target_id])
  6. @hash = params[:hash]
  7. if MergeUsersService.validate(source_user: @source_user, target_user: @target_user, hash: @hash)
  8. render :confirm
  9. else
  10. respond_with_error(status: 422)
  11. end
  12. end
  13. 1 def merge
  14. 2 @source_user = User.active.find_by!(id: params[:source_id])
  15. 2 @target_user = User.active.find_by!(id: params[:target_id])
  16. 2 @hash = params[:hash]
  17. 2 if MergeUsersService.validate(source_user: @source_user, target_user: @target_user, hash: @hash)
  18. 1 MigrateUserWorker.perform_async(@source_user.id, @target_user.id)
  19. 1 render :complete
  20. else
  21. 1 respond_with_error(status: 422)
  22. end
  23. end
  24. end

app/controllers/pie_chart_controller.rb

0.0% lines covered

18 relevant lines. 0 lines covered and 18 lines missed.
    
  1. require_relative Rails.root.join('lib/pie_chart')
  2. class PieChartController < ApplicationController
  3. def show
  4. scores = params[:scores].to_s.split(',').map(&:to_i)
  5. colors = params[:colors].to_s.split(',').map {|c| "##{c}"}
  6. svg = PieChartSVG.from_primitives(scores, colors)
  7. name = scores.join() + colors.join()
  8. file = Tempfile.new('name')
  9. file.write(svg.render)
  10. file.rewind
  11. png = ImageProcessing::MiniMagick
  12. .source(file)
  13. .convert("png")
  14. .call
  15. file.unlink
  16. send_file(png, type: 'image/png', disposition: 'inline')
  17. end
  18. end

app/controllers/poll_templates_controller.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class PollTemplatesController < ApplicationController
  2. # TODO remove this file
  3. def dump_i18n
  4. group = load_and_authorize :group, :export
  5. templates = {}
  6. PollTemplate.where(group_id: group.id).order(:position).each do |pt|
  7. templates = templates.merge(pt.dump_i18n)
  8. end
  9. render plain: templates.to_yaml, layout: false, template: nil
  10. end
  11. end

app/controllers/polls_controller.rb

78.95% lines covered

19 relevant lines. 15 lines covered and 4 lines missed.
    
  1. 1 class PollsController < ApplicationController
  2. 1 include UsesMetadata
  3. 1 include LoadAndAuthorize
  4. 1 include EmailHelper
  5. 1 helper :email
  6. 1 def export
  7. 3 @exporter = PollExporter.new(load_and_authorize(:poll, :export))
  8. 2 @recipient = current_user
  9. 2 @action_name = :export
  10. 2 respond_to do |format|
  11. 2 format.html
  12. 3 format.csv { send_data @exporter.to_csv, filename:@exporter.file_name }
  13. end
  14. end
  15. 1 def example
  16. if poll = PollGenerator.new(params[:type]).generate!
  17. redirect_to poll
  18. else
  19. redirect_to root_path, notice: "Sorry, we don't know about that poll type"
  20. end
  21. end
  22. 1 private
  23. 1 def current_user
  24. restricted_user || super
  25. end
  26. end

app/controllers/received_emails_controller.rb

94.12% lines covered

17 relevant lines. 16 lines covered and 1 lines missed.
    
  1. 1 class ReceivedEmailsController < ApplicationController
  2. 1 skip_before_action :verify_authenticity_token
  3. 1 def create
  4. 13 email = build_received_email_from_params
  5. 13 if email.is_addressed_to_loomio? && !email.is_auto_response?
  6. 13 email.save!
  7. 13 ReceivedEmailService.route(email)
  8. end
  9. 13 head :ok
  10. end
  11. 1 private
  12. 1 def build_received_email_from_params
  13. 13 data = JSON.parse(params[:mailinMsg])
  14. 13 email = ReceivedEmail.new(
  15. headers: data['headers'],
  16. body_text: data['text'],
  17. body_html: data['html'],
  18. 13 dkim_valid: data['dkim'] == 'pass' ? true : false,
  19. 13 spf_valid: data['spf'] == 'pass' ? true : false,
  20. )
  21. 13 email.attachments = data.fetch('attachments', []).map do |a|
  22. {
  23. io: StringIO.new(Base64.decode64(params[a['generatedFileName']])),
  24. content_type: a['contentType'],
  25. filename: a['generatedFileName']
  26. }
  27. end
  28. 13 email
  29. end
  30. end

app/controllers/redirect_controller.rb

83.33% lines covered

12 relevant lines. 10 lines covered and 2 lines missed.
    
  1. 1 class RedirectController < ApplicationController
  2. 1 def group
  3. 1 redirect
  4. end
  5. 1 def discussion
  6. 1 redirect
  7. end
  8. 1 def poll
  9. redirect
  10. end
  11. 1 private
  12. 1 def redirect(model: action_name, to: ModelLocator.new(model, params).locate)
  13. 2 if to.present?
  14. 2 redirect_to send(:"#{model}_url", to), status: :moved_permanently
  15. else
  16. respond_with_error message: :"errors.not_found", status: 404
  17. end
  18. end
  19. end

app/controllers/robots_controller.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class RobotsController < ActionController::Base
  2. respond_to :text
  3. def show
  4. if ENV['ALLOW_ROBOTS']
  5. render :public_robots
  6. else
  7. render :private_robots
  8. end
  9. end
  10. end

app/controllers/root_controller.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class RootController < ApplicationController
  2. def index
  3. redirect_to dashboard_path
  4. end
  5. end

app/controllers/thread_templates_controller.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class ThreadTemplatesController < ApplicationController
  2. # TODO remove this file
  3. def dump_i18n
  4. @group = load_and_authorize(:group, :export)
  5. templates = {}
  6. DiscussionTemplate.where(group_id: @group.id).order(:position).each do |dt|
  7. templates = templates.merge(dt.dump_i18n)
  8. end
  9. render plain: templates.to_yaml, layout: false, template: nil
  10. end
  11. end

app/controllers/users/passwords_controller.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. class Users::PasswordsController < Devise::PasswordsController
  2. def update
  3. self.resource = resource_class.reset_password_by_token(resource_params)
  4. if resource.errors.empty?
  5. set_flash_message!(:notice, :updated)
  6. sign_in(resource)
  7. respond_with resource, location: after_sign_in_path_for(resource)
  8. else
  9. set_minimum_password_length
  10. respond_with resource
  11. end
  12. end
  13. private
  14. def require_no_authentication
  15. # noop
  16. end
  17. end

app/controllers/users_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class UsersController < ApplicationController
  2. 1 include UsesMetadata
  3. end

app/extras/app_config.rb

0.0% lines covered

104 relevant lines. 0 lines covered and 104 lines missed.
    
  1. class AppConfig
  2. CONFIG_FILES = %w(
  3. webhook_event_kinds
  4. colors
  5. emojis
  6. poll_types
  7. poll_templates
  8. discussion_templates
  9. providers
  10. doctypes
  11. locales
  12. )
  13. BANNED_CHARS = %(\\s:,;'"`<>)
  14. EMAIL_REGEX = /[^#{BANNED_CHARS}]+?@[^#{BANNED_CHARS}]+\.[^#{BANNED_CHARS}]+/
  15. URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/
  16. CONFIG_FILES.each do |config|
  17. define_singleton_method(config) do
  18. instance_variable_get(:"@#{config}") ||
  19. instance_variable_set(:"@#{config}", YAML.load_file(Rails.root.join("config", "#{config}.yml")))
  20. end
  21. end
  22. def self.release
  23. @release ||= begin
  24. (`git rev-parse HEAD`.strip.presence || File.mtime("app").to_i.to_s)
  25. end
  26. end
  27. def self.image_regex
  28. doctypes.detect { |type| type['name'] == 'image' }['regex']
  29. end
  30. def self.theme
  31. brand_colors = {
  32. gold: "#DCA034",
  33. ink: "#293C4A",
  34. wellington: "#7F9EA0",
  35. sunset: "#E4C2B9",
  36. sky: "#658AE7",
  37. rock: "#C77C3B",
  38. white: "#FFFFFF"
  39. }
  40. # here are some useful variations on these colours
  41. # https://maketintsandshades.com/#DCA034,293C4A,7F9EA0,E4C2B9,658AE7,C77C3B
  42. logo_color = :gold
  43. {
  44. brand_colors: brand_colors,
  45. site_name: ENV.fetch('SITE_NAME', 'Loomio'),
  46. channels_uri: ENV.fetch('CHANNELS_URI', 'ws://localhost:5000'),
  47. terms_url: ENV['TERMS_URL'],
  48. privacy_url: ENV['PRIVACY_URL'],
  49. canonical_host: ENV['CANONICAL_HOST'],
  50. reply_hostname: ENV['REPLY_HOSTNAME'],
  51. help_url: ENV.fetch('HELP_URL', 'https://help.loomio.com/'),
  52. icon_src: ENV.fetch('THEME_ICON_SRC', "/brand/icon_#{logo_color}_150h.png"),
  53. app_logo_src: ENV.fetch('THEME_APP_LOGO_SRC', "/brand/logo_#{logo_color}.svg"),
  54. apple_touch_src: ENV.fetch('APPLE_TOUCH_SRC', "/brand/touch_icon_gold.png"),
  55. default_group_cover_src: ENV.fetch('THEME_DEFAULT_GROUP_COVER_SRC', '/theme/default_group_cover.png'),
  56. saml_login_provider_name: ENV.fetch('SAML_LOGIN_PROVIDER_NAME', 'SAML'),
  57. oauth_login_provider_name: ENV.fetch('OAUTH_LOGIN_PROVIDER_NAME', 'OAUTH'),
  58. # used in emails
  59. email_header_logo_src: ENV.fetch('THEME_EMAIL_HEADER_LOGO_SRC', "/brand/logo_#{logo_color}_96h.png"),
  60. email_footer_logo_src: ENV.fetch('THEME_EMAIL_FOOTER_LOGO_SRC', "/brand/logo_#{logo_color}_48h.png"),
  61. primary_color: ENV.fetch('THEME_PRIMARY_COLOR', brand_colors[:sky]),
  62. accent_color: ENV.fetch('THEME_ACCENT_COLOR', brand_colors[:gold]),
  63. text_on_primary_color: ENV.fetch('THEME_TEXT_ON_PRIMARY_COLOR', '#ffffff'),
  64. text_on_accent_color: ENV.fetch('THEME_TEXT_ON_ACCENT_COLOR', '#ffffff'),
  65. vuetify: {
  66. primary: ENV.fetch('THEME_COLOR_PRIMARY', brand_colors[:sky]),
  67. secondary: ENV.fetch('THEME_COLOR_SECONDARY', brand_colors[:sunset]),
  68. accent: ENV.fetch('THEME_COLOR_ACCENT', brand_colors[:gold]),
  69. error: ENV.fetch('THEME_COLOR_ERROR', nil),
  70. warning: ENV.fetch('THEME_COLOR_WARNING', nil),
  71. info: ENV.fetch('THEME_COLOR_INFO', brand_colors[:sky]),
  72. success: ENV.fetch('THEME_COLOR_SUCCESS', nil),
  73. anchor: ENV.fetch('THEME_COLOR_ANCHOR', brand_colors[:sky])
  74. }
  75. }
  76. end
  77. def self.app_features
  78. {
  79. env: Rails.env,
  80. subscriptions: !!ENV.fetch('CHARGIFY_API_KEY', false),
  81. demos: ENV.fetch('FEATURES_DEMO_GROUPS', false),
  82. trials: ENV.fetch('FEATURES_TRIALS', false),
  83. trial_days: ENV.fetch('TRIAL_DAYS', nil),
  84. new_thread_button: !!ENV.fetch('FEATURES_NEW_THREAD_BUTTON', false),
  85. email_login: !ENV['FEATURES_DISABLE_EMAIL_LOGIN'],
  86. create_user: !ENV['FEATURES_DISABLE_CREATE_USER'],
  87. create_group: !ENV['FEATURES_DISABLE_CREATE_GROUP'],
  88. public_groups: !ENV['FEATURES_DISABLE_PUBLIC_GROUPS'],
  89. help_link: !ENV['FEATURES_DISABLE_HELP_LINK'],
  90. example_content: !ENV['FEATURES_DISABLE_EXAMPLE_CONTENT'],
  91. explore_public_groups: ENV.fetch('FEATURES_EXPLORE_PUBLIC_GROUPS', false),
  92. template_gallery: ENV.fetch('FEATURES_TEMPLATE_GALLERY', false),
  93. show_contact: ENV.fetch('FEATURES_SHOW_CONTACT', false),
  94. show_contact_consent: ENV.fetch('FEATURES_SHOW_CONTACT_CONSENT', false),
  95. sentry_sample_rate: ENV.fetch('SENTRY_SAMPLE_RATE', 0.1).to_f,
  96. hidden_poll_templates: %w[proposal question],
  97. transcription: TranscriptionService.available?
  98. }
  99. end
  100. def self.json_parse_or_false(name)
  101. if ENV[name]
  102. JSON.parse(ENV[name])
  103. else
  104. false
  105. end
  106. end
  107. end

app/extras/clients/base.rb

77.36% lines covered

53 relevant lines. 41 lines covered and 12 lines missed.
    
  1. 1 class Clients::Base
  2. 1 attr_reader :key
  3. 1 def self.instance
  4. 2 new(
  5. key: ENV["#{name.demodulize.upcase}_APP_KEY"],
  6. secret: ENV["#{name.demodulize.upcase}_APP_SECRET"]
  7. )
  8. end
  9. 1 def initialize(key: nil, secret: nil, token: nil)
  10. 40 @key = key
  11. 40 @secret = secret
  12. 40 @token = token
  13. end
  14. 1 def get(path, params: {}, headers: {}, options: {})
  15. perform :get, path, params, headers, options.merge(params_field: :query)
  16. end
  17. 1 def post(path, params: {}, headers: {}, options: {})
  18. 38 perform :post, path, params, headers, options.merge(params_field: :body)
  19. end
  20. 1 def post_query(path, params: {}, headers: {}, options: {})
  21. perform :post, path, params, headers, options.merge(params_field: :query)
  22. end
  23. # make request for initial user information
  24. # overwrite if the API has a different endpoint to get a user
  25. 1 def fetch_user_info
  26. get "me"
  27. end
  28. 1 def scope_query_param
  29. scope.join(',')
  30. end
  31. 1 def client_key_name
  32. :client_id
  33. end
  34. 1 def scope
  35. []
  36. end
  37. 1 private
  38. 1 def perform(method, path, params, headers, options)
  39. 38 options.reverse_merge!(
  40. host: default_host,
  41. success: default_success,
  42. failure: default_failure,
  43. is_success: default_is_success
  44. )
  45. 38 Clients::Request.new(method, [options[:host], path].compact.join('/'), {
  46. options[:params_field] => params_for(params),
  47. :"headers" => headers_for(headers)
  48. 38 }).tap { |request| request.perform!(options) }
  49. end
  50. 1 def params_for(params = {})
  51. 38 if require_json_payload?
  52. 38 default_params.merge(params).to_json
  53. else
  54. default_params.merge(params)
  55. end
  56. end
  57. 1 def headers_for(headers = {})
  58. 38 default_headers.merge(headers)
  59. end
  60. 1 def require_json_payload?
  61. false
  62. end
  63. # determines whether the response should be deemed successful or not
  64. # we override this for things like requesting permissions from facebook,
  65. # where the response comes back with status 200, but the permissions contained
  66. # within aren't sufficient to operate the API
  67. 1 def default_is_success
  68. 76 ->(response) { response.success? }
  69. end
  70. 1 def default_success
  71. 38 ->(response) { response }
  72. end
  73. 1 def default_failure
  74. 38 ->(response) {
  75. Rails.logger.info "Failed #{self.class.name.demodulize} api request. response: #{response} token: #{@token}"
  76. response
  77. }
  78. end
  79. 1 def default_params
  80. 152 { client_id: @key, client_secret: @secret, token_name => @token }.delete_if { |k,v| v.nil? }
  81. end
  82. 1 def default_headers
  83. 38 { 'Content-Type' => 'application/json; charset=utf-8' }
  84. end
  85. 1 def token_name
  86. 38 :token
  87. end
  88. 1 def post_content!(event)
  89. raise NotImplementedError.new
  90. end
  91. 1 def default_host
  92. raise NotImplementedError.new
  93. end
  94. end

app/extras/clients/google.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. class Clients::Google < Clients::Base
  2. def fetch_access_token(code, uri)
  3. post "token", params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
  4. end
  5. def fetch_user_info
  6. get "userinfo", options: { host: :"https://www.googleapis.com/oauth2/v2" }
  7. end
  8. def scope
  9. %w(email profile).freeze
  10. end
  11. private
  12. def default_headers
  13. { 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
  14. end
  15. def token_name
  16. :oauth_token
  17. end
  18. def default_host
  19. "https://www.googleapis.com/oauth2/v4".freeze
  20. end
  21. end

app/extras/clients/nextcloud.rb

0.0% lines covered

28 relevant lines. 0 lines covered and 28 lines missed.
    
  1. class Clients::Nextcloud < Clients::Base
  2. def fetch_access_token(code, uri)
  3. post 'index.php/apps/oauth2/api/v1/token', params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
  4. end
  5. def fetch_user_info
  6. get 'ocs/v2.php/cloud/user', params: { format: :json }
  7. end
  8. private
  9. def default_params
  10. { client_id: @key, client_secret: @secret }.delete_if { |k,v| v.nil? }
  11. end
  12. def authorization_headers
  13. { 'Authorization' => "Bearer #{@token}" }
  14. end
  15. def common_headers
  16. { 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
  17. end
  18. def default_headers
  19. if @token
  20. common_headers.merge(authorization_headers)
  21. else
  22. common_headers
  23. end
  24. end
  25. def default_host
  26. ENV['NEXTCLOUD_HOST']
  27. end
  28. end

app/extras/clients/oauth.rb

0.0% lines covered

36 relevant lines. 0 lines covered and 36 lines missed.
    
  1. class Clients::Oauth < Clients::Base
  2. def fetch_access_token(code, uri)
  3. post ENV.fetch('OAUTH_TOKEN_URL'), params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
  4. end
  5. def fetch_user_info
  6. get ENV.fetch('OAUTH_PROFILE_URL')
  7. end
  8. private
  9. def perform(method, url, params, headers, options)
  10. options.reverse_merge!(
  11. success: default_success,
  12. failure: default_failure,
  13. is_success: default_is_success
  14. )
  15. Clients::Request.new(method, url, {
  16. options[:params_field] => params_for(params),
  17. :"headers" => headers_for(headers)
  18. }).tap { |request| request.perform!(options) }
  19. end
  20. def default_params
  21. { client_id: @key, client_secret: @secret }.delete_if { |k,v| v.nil? }
  22. end
  23. def authorization_headers
  24. { 'Authorization' => "Bearer #{@token}" }
  25. end
  26. def common_headers
  27. { 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
  28. end
  29. def default_headers
  30. if @token
  31. common_headers.merge(authorization_headers)
  32. else
  33. common_headers
  34. end
  35. end
  36. end

app/extras/clients/recaptcha.rb

50.0% lines covered

10 relevant lines. 5 lines covered and 5 lines missed.
    
  1. 1 class Clients::Recaptcha < Clients::Base
  2. 1 def validate(recaptcha)
  3. req = post_query "siteverify", params: { response: recaptcha, secret: ENV['RECAPTCHA_SECRET_KEY']}
  4. Rails.logger.info "recaptcha response #{req.response}"
  5. req.response['success']
  6. end
  7. 1 private
  8. 1 def default_host
  9. "https://www.google.com/recaptcha/api"
  10. end
  11. 1 def default_is_success
  12. ->(response) { response.success? && JSON.parse(response.body)['success'].present? }
  13. end
  14. end

app/extras/clients/request.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 Clients::Request = Struct.new(:method, :url, :params) do
  2. 1 include HTTParty
  3. 1 default_options.update(verify: false) if ENV['SSL_VERIFY_FALSE']
  4. 1 default_options.update(verify_peer: false) if ENV['SSL_VERIFY_PEER_FALSE']
  5. 1 debug_output $stdout
  6. 1 attr_accessor :callback, :success
  7. 1 def json
  8. @json ||= callback.call JSON.parse(response.body)
  9. end
  10. 1 def perform!(options = {})
  11. 38 self.success = options[:is_success].call(response)
  12. 38 self.callback = options[success ? :success : :failure]
  13. end
  14. 1 def response
  15. 76 @response ||= self.class.send(method, url, params)
  16. end
  17. end

app/extras/clients/slack.rb

0.0% lines covered

45 relevant lines. 0 lines covered and 45 lines missed.
    
  1. class Clients::Slack < Clients::Base
  2. def fetch_access_token(code, uri)
  3. get "oauth.access", params: { code: code, redirect_uri: uri }
  4. end
  5. def fetch_user_info
  6. get "users.profile.get", options: { success: ->(response) { response['profile'] } }
  7. end
  8. def fetch_team_info
  9. get "team.info", options: { success: ->(response) { response['team'] } }
  10. end
  11. # We are doing two requests and combining them here
  12. def fetch_channels
  13. channels = get "conversations.list", params: {types: "public_channel,private_channel", limit: 1000}, options: { success: ->(response) { response['channels'] } }
  14. json = if channels.success
  15. [channels].map(&:json).flatten.reject {|channel| channel['name'].starts_with?("mpdm-") }.sort_by {|channel| channel['name'].downcase }
  16. else
  17. []
  18. end
  19. OpenStruct.new(json: json)
  20. end
  21. def post_content!(event)
  22. get "chat.postMessage", params: serialized_event(event)
  23. end
  24. def is_member_of?(channel_id, uid)
  25. get "channels.info", params: { channel: channel_id }, options: {
  26. success: ->(response) { Array(response['channel']['members']).include?(uid) } }
  27. end
  28. def scope
  29. %w(users.profile:read channels:read groups:read team:read chat:write:bot commands)
  30. end
  31. private
  32. def serialized_event(event)
  33. serializer = [
  34. "Slack::#{event.kind.classify}Serializer",
  35. "Slack::#{event.eventable.class}Serializer",
  36. "Slack::BaseSerializer"
  37. ].detect { |str| str.constantize rescue nil }.constantize
  38. serializer.new(event, root: false).as_json
  39. end
  40. def default_is_success
  41. ->(response) { response.success? && JSON.parse(response.body)['ok'].present? }
  42. end
  43. def default_host
  44. "https://slack.com/api".freeze
  45. end
  46. end

app/extras/clients/webhook.rb

60.0% lines covered

10 relevant lines. 6 lines covered and 4 lines missed.
    
  1. 1 class Clients::Webhook < Clients::Base
  2. 1 def post_content!(event, format, webhook)
  3. post @token, params: serialized_event(event, format, webhook)
  4. end
  5. 1 def default_host
  6. nil
  7. end
  8. 1 def require_json_payload?
  9. 38 true
  10. end
  11. 1 def serialized_event(event, format, webhook)
  12. serializer = [
  13. "Webhook::#{format.classify}::#{event.kind.classify}Serializer",
  14. "Webhook::#{format.classify}::#{event.eventable.class}Serializer",
  15. "Webhook::#{format.classify}::BaseSerializer"
  16. ].detect { |str| str.constantize rescue nil }.constantize
  17. serializer.new(event, root: false, scope: {webhook: webhook}).as_json
  18. end
  19. end

app/extras/group_exporter.rb

56.0% lines covered

25 relevant lines. 14 lines covered and 11 lines missed.
    
  1. 1 class GroupExporter
  2. 1 attr_accessor :group
  3. EXPORT_MODELS = {
  4. 1 groups: %w[id key name description created_at],
  5. memberships: %w[group_id user_id user_name user_email admin created_at accepted_at],
  6. discussions: %w[id group_id author_id author_name title description created_at],
  7. comments: %w[id group_id discussion_id author_id author_name title author_name body created_at],
  8. polls: %w[id key discussion_id group_id author_id author_name title details closing_at closed_at created_at poll_type custom_fields],
  9. stances: %w[id poll_id participant_id author_name reason latest created_at updated_at],
  10. outcomes: %w[id poll_id author_id statement created_at updated_at]
  11. }.freeze
  12. 1 EXPORT_MODELS.keys.each do |model|
  13. 7 define_method model, -> {
  14. instance_variable_get(:"@#{model}") ||
  15. instance_variable_set(:"@#{model}", models_for(model))
  16. }
  17. 7 define_method :"#{model.to_s.singularize}_fields", -> { EXPORT_MODELS[model] }
  18. end
  19. 1 attr_reader :field_names
  20. 1 def initialize(group)
  21. 1 @group = group
  22. 1 @field_names = {}
  23. end
  24. 1 def to_csv(opts = {})
  25. CSV.generate(**opts) do |csv|
  26. csv << ["Export for #{@group.full_name}"]
  27. csv << []
  28. EXPORT_MODELS.keys.each do |model|
  29. csv_append(
  30. csv: csv,
  31. fields: send(:"#{model.to_s.singularize}_fields"),
  32. models: send(:"#{model}"),
  33. title: model.to_s.humanize
  34. )
  35. end
  36. end
  37. end
  38. 1 private
  39. 1 def models_for(model)
  40. model.to_s.classify.constantize.in_organisation(@group).order(created_at: :asc)
  41. end
  42. 1 def csv_append(csv:, fields:, models:, title:)
  43. csv << ["#{title} (#{models.length})"]
  44. csv << fields.map(&:humanize)
  45. models.each { |model| csv << fields.map { |field| model.send(field) } }
  46. csv << []
  47. end
  48. end

app/extras/model_locator.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 ModelLocator = Struct.new(:model, :params) do
  2. 1 def locate
  3. 490 return nil unless defined?(resource_class)
  4. 490 if model.to_sym == :user
  5. 5 resource_class.verified.find_by(username: params[:id] || params[:username]) || resource_class.friendly.find(params[:id] || params[:user_id])
  6. 485 elsif model.to_sym == :group
  7. 119 (id_param && resource_class.find_by(id: id_param)) ||
  8. 27 (key_param && resource_class.find_by(key: key_param)) ||
  9. resource_class.where.not(handle: nil).find_by(handle: params[:id])
  10. 366 elsif resource_class.respond_to?(:friendly)
  11. 216 resource_class.friendly.find key_or_id
  12. else
  13. 150 resource_class.find key_or_id
  14. end
  15. end
  16. 1 def locate!
  17. 466 locate or raise ActiveRecord::RecordNotFound
  18. end
  19. 1 private
  20. 1 def id_param
  21. 211 key_or_id.to_i.to_s == key_or_id && key_or_id
  22. end
  23. 1 def key_param
  24. 54 key_or_id.to_i.to_s != key_or_id && key_or_id
  25. end
  26. 1 def key_or_id
  27. 1134 (params[:"#{model}_id"] || params[:"#{model}_key"] || params[:key] || params[:id]).to_s
  28. end
  29. 1 def resource_class
  30. 863 @resource_class ||= model.to_s.camelize.constantize
  31. end
  32. end

app/extras/poll_exporter.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 1 class PollExporter
  2. 1 include Routing
  3. 1 def initialize(poll)
  4. 2 @poll = poll
  5. end
  6. 1 def file_name
  7. 1 "poll-#{@poll.id}-#{@poll.key}-#{@poll.title.parameterize}.csv"
  8. end
  9. 1 def meta_table
  10. 2 outcome = @poll.current_outcome
  11. {
  12. 2 id: @poll.id,
  13. group_id: @poll.group_id,
  14. discussion_id: @poll.discussion_id,
  15. author_id: @poll.author.id,
  16. title: @poll.title,
  17. author_name: @poll.author.name,
  18. created_at: @poll.created_at,
  19. closed_at: @poll.closed_at,
  20. decided_voters_count: @poll.decided_voters_count,
  21. undecided_voters_count: @poll.undecided_voters_count,
  22. voters_count: @poll.voters_count,
  23. details: @poll.details,
  24. group_name: @poll.group&.full_name,
  25. discussion_title: @poll.discussion&.title,
  26. outcome_author_id: outcome&.author_id,
  27. outcome_author_name: outcome&.author&.name,
  28. outcome_created_at: outcome&.created_at,
  29. outcome_statement: outcome&.statement,
  30. poll_url: poll_url(@poll)
  31. }.compact
  32. end
  33. 1 def to_csv(opts={})
  34. 1 CSV.generate do |csv|
  35. 1 csv << ['poll']
  36. 1 csv << meta_table.keys
  37. 1 csv << meta_table.values
  38. 1 csv << ['poll_options']
  39. 1 results = PollService.calculate_results(@poll, @poll.poll_options)
  40. 1 keys = %w[id poll_id name name_format rank score score_percent max_score_percent voter_percent average voter_count color]
  41. 1 csv << keys
  42. 3 results.each { |r| csv << r.slice(*keys).values }
  43. 1 csv << ['votes']
  44. 1 csv << ['id', 'poll_id', 'voter_id', 'voter_name', 'created_at', 'updated_at', 'reason', 'reason_format'] + @poll.poll_option_names
  45. 1 @poll.stances.latest.each do |stance|
  46. line = [
  47. 3 stance.id,
  48. stance.poll_id,
  49. stance.participant_id,
  50. stance.author_name,
  51. stance.created_at&.iso8601,
  52. stance.updated_at&.iso8601,
  53. stance.reason,
  54. stance.reason_format]
  55. 3 @poll.poll_options.each do |poll_option|
  56. 3 line.push(stance.option_scores[poll_option.id.to_s] || nil)
  57. end
  58. 3 csv << line
  59. end
  60. end
  61. end
  62. end

app/extras/queries/admin_group_page.rb

0.0% lines covered

45 relevant lines. 0 lines covered and 45 lines missed.
    
  1. class Queries::AdminGroupPage
  2. def self.members_per_day_sql(group)
  3. "select date_trunc('day', created_at) date, count(distinct user_id) from memberships where group_id = #{group.id} group by date order by date"
  4. end
  5. def self.threads_per_day_sql(group)
  6. "select date_trunc('day', created_at) date, count(id) from discussions where group_id IN (#{group.id_and_subgroup_ids.join(',')}) group by date order by date"
  7. end
  8. def self.polls_per_day_sql(group)
  9. "select date_trunc('day', created_at) date, count(id) from polls where group_id IN (#{group.id_and_subgroup_ids.join(',')}) group by date order by date"
  10. end
  11. def self.thread_events_per_day(group)
  12. ids = Discussion.where(group_id: group.id_and_subgroup_ids).pluck(:id)
  13. if ids.any?
  14. "select date_trunc('day', created_at) date, count(id) from events where discussion_id IN (#{ids.join(',')}) group by date order by date"
  15. else
  16. "select date_trunc('day', created_at) date, count(id) from events where discussion_id = 0 group by date order by date"
  17. end
  18. end
  19. def self.execute(sql)
  20. ActiveRecord::Base.connection.execute(sql)
  21. end
  22. def self.run_per_day(sql)
  23. massage execute(sql)
  24. end
  25. def self.massage(records)
  26. records.to_a.map { |record| { date: record["date"], count: record["count"] } }
  27. end
  28. def self.fetch_data(group)
  29. {
  30. events: run_per_day(thread_events_per_day(group)),
  31. members: run_per_day(members_per_day_sql(group)),
  32. threads: run_per_day(threads_per_day_sql(group)),
  33. polls: run_per_day(polls_per_day_sql(group))
  34. }
  35. end
  36. def self.thread_items_count(group)
  37. Discussion.where(group_id: group.id_and_subgroup_ids).sum(:items_count)
  38. end
  39. def self.discussions_count(group)
  40. Discussion.where(group_id: group.id_and_subgroup_ids).count
  41. end
  42. def self.polls_count(group)
  43. Poll.where(group_id: group.id_and_subgroup_ids).count
  44. end
  45. end

app/extras/queries/explore_groups.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. 1 class Queries::ExploreGroups < Delegator
  2. 1 def initialize
  3. 11 min_members = ENV.fetch('EXPLORE_MIN_MEMBERS', 0)
  4. 11 min_threads = ENV.fetch('EXPLORE_MIN_THREADS', 0)
  5. 11 require_subscription = ENV.fetch('EXPLORE_REQUIRE_SUBSCRIPTION', false)
  6. 11 @relation = Group.where(listed_in_explore: true)
  7. .parents_only
  8. .published
  9. .where('groups.name IS NOT NULL')
  10. .where('groups.memberships_count > ?', min_members)
  11. .where('groups.discussions_count > ?', min_threads)
  12. 11 if require_subscription
  13. @relation = @relation.eager_load(:subscription)
  14. .where("subscriptions.state = 'active'")
  15. end
  16. 11 @relation
  17. end
  18. 1 def search_for(q)
  19. 6 @relation = @relation.explore_search(q) if q.present?
  20. 6 self
  21. end
  22. 1 def __getobj__
  23. 42 @relation
  24. end
  25. end

app/extras/queries/group_stats.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class Queries::GroupStats
  2. def self.comments_count(group_ids)
  3. Discussion.where(group_id: group_ids).sum { |d| d.comments.count }
  4. end
  5. def self.discussions_count(group_ids)
  6. Discussion.where(group_id: group_ids).count
  7. end
  8. def self.polls_count(group_ids)
  9. Poll.where(group_id: group_ids).count
  10. end
  11. def self.voters_count(group_ids)
  12. Poll.where(group_id: group_ids).sum(:voters_count)
  13. end
  14. def self.poll_types_count(group_ids)
  15. Poll.where(group_id: group_ids).group(:poll_type).count
  16. end
  17. end

app/extras/queries/union_query.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Queries::UnionQuery
  2. 1 def self.for(table_name, queries)
  3. 8 from = Array(queries).map(&:to_sql).map(&:presence).compact.join(" UNION ")
  4. 8 table_name.to_s.singularize.camelize.constantize.distinct.from("(#{from}) as #{table_name}")
  5. end
  6. end

app/extras/queries/users_by_volume_query.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Queries::UsersByVolumeQuery
  2. 1 def self.normal_or_loud(model)
  3. 680 users_by_volume(model, '>=', DiscussionReader.volumes[:normal])
  4. end
  5. 1 def self.email_notifications(model)
  6. 673 normal_or_loud(model)
  7. end
  8. 1 def self.app_notifications(model)
  9. 740 users_by_volume(model, '>=', DiscussionReader.volumes[:quiet])
  10. end
  11. 1 %w(mute quiet normal loud).map(&:to_sym).each do |volume|
  12. 4 define_singleton_method volume, ->(model) {
  13. 257 users_by_volume(model, '=', DiscussionReader.volumes[volume])
  14. }
  15. end
  16. 1 private
  17. 1 def self.users_by_volume(model, operator, volume)
  18. 1677 return User.none if model.nil?
  19. 1676 User.active.distinct.
  20. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{model.discussion_id || 0} AND dr.user_id = users.id").
  21. joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{model.group_id || 0}").
  22. joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{model.poll_id || 0} AND s.latest = TRUE").
  23. where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
  24. (dr.id IS NOT NULL AND dr.guest = TRUE AND dr.revoked_at IS NULL) OR
  25. (s.id IS NOT NULL AND s.guest = TRUE AND s.revoked_at IS NULL) OR
  26. (m.id IS NULL and dr.id IS NULL and s.id IS NULL)').
  27. where("coalesce(s.volume, dr.volume, m.volume, 2) #{operator} :volume", volume: volume)
  28. end
  29. end

app/extras/range_set.rb

100.0% lines covered

68 relevant lines. 68 lines covered and 0 lines missed.
    
  1. 1 class RangeSet
  2. 1 def self.includes?(haystack, needle)
  3. 437 to_ranges(needle).all? do |a|
  4. 496 to_ranges(haystack).any? { |b| range_includes?(b, a) }
  5. end
  6. end
  7. 1 def self.range_includes?(a, b)
  8. 66 return false if (a.length == 0 || b.length == 0)
  9. 66 a[0] <= b[0] && a[1] >= b[1]
  10. end
  11. 1 def self.length(ranges)
  12. 13 ranges.map {|range| range[1] - range[0] + 1}.sum
  13. end
  14. # do 2 ranges overlap?
  15. 1 def self.overlaps?(a,b)
  16. 27 sorted = [a,b].sort_by{|r| r[0] }
  17. 9 sorted[0][1] >= sorted[1][0]
  18. end
  19. 1 def self.to_ranges(ranges)
  20. # ranges is supposed to be an array of ranges.
  21. # but it's useful to support
  22. # range set
  23. 1297 return [] if ranges.nil?
  24. # single id
  25. 1297 return [[ranges, ranges]] if ranges.is_a? Numeric
  26. # single range
  27. 457 return [[ranges.first, ranges.last]] if ranges.is_a? Range
  28. # array of ids
  29. 476 return ranges.map {|id| [id,id] } if ranges.is_a?(Array) && ranges.first.is_a?(Numeric)
  30. # serialized array of range pairs
  31. 448 return parse(ranges) if ranges.is_a? String
  32. # as well as a well formatted array of ranges
  33. 445 ranges
  34. end
  35. 1 def self.intersect_ranges(ranges1, ranges2)
  36. 41 set1 = ranges_to_list(ranges1)
  37. 41 set2 = ranges_to_list(ranges2)
  38. 81 ranges_from_list(set1.select { |id| set2.include? id })
  39. end
  40. 1 def self.ranges_to_list(ranges)
  41. 164 ranges.map {|range| (range[0]..range[1]).to_a}.flatten
  42. end
  43. 1 def self.subtract_range(whole, part)
  44. # examples
  45. # read nothing
  46. 378 return [whole] if part.empty? || (part.first > whole.last) || (part.last < whole.first)
  47. # read the whole thing
  48. # range [2,3]
  49. # read_range [2,3] or [1,4]
  50. # unread_ranges []
  51. 51 return [] if (part.first <= whole.first) && (part.last >= whole.last)
  52. # read the middle
  53. # range [1,3]
  54. # read_range [2,2]
  55. # unread_ranges [[1,1],[3,3]]
  56. 49 return [[whole.first, part.first - 1], [part.last + 1, whole.last]] if (part.first > whole.first) && (part.last < whole.last)
  57. # read the first part
  58. # range [1,3]
  59. # read_range [1,2]
  60. # unread_ranges [[3,3]]
  61. 11 return [[part.last + 1, whole.last]] if (part.first == whole.first) && (part.last < whole.last)
  62. # read the last part
  63. # range [1,3]
  64. # read_range [2,3]
  65. # unread_ranges [[1,1]]
  66. # start of unread_range is either same as range.first or read_range.last
  67. 5 return [[whole.first, part.first - 1]] if (part.first > whole.first) && (part.last == whole.last)
  68. end
  69. # all ranges: [[1,2]] , some ranges: [[1,1]]
  70. 1 def self.subtract_ranges(wholes, parts)
  71. 8 wholes = reduce(wholes)
  72. 8 parts = reduce(parts)
  73. 8 parts.each do |part|
  74. 48 output = []
  75. 48 wholes.each do |whole|
  76. 372 output = reduce output.concat(subtract_range(whole, part))
  77. end
  78. 48 wholes = output
  79. end
  80. 8 wholes
  81. end
  82. # for turning an array of likely to be sequential ids into ranges (eg: pluck -> ranges)
  83. 1 def self.ranges_from_list(ids)
  84. 1313 return [] if ids.empty?
  85. 919 ranges = []
  86. 919 last_id = ids.first
  87. 919 first_id = ids.first
  88. 919 ids.each do |id|
  89. 1862 if id == last_id + 1
  90. 934 last_id = id
  91. else
  92. 928 ranges << [first_id,last_id]
  93. 928 first_id = id
  94. 928 last_id = id
  95. end
  96. end
  97. 919 ranges << [first_id,last_id]
  98. 919 reduce ranges
  99. end
  100. 1 def self.parse(string)
  101. # ranges string format [[1,2], [4,5]] == 1-2,4-5
  102. 2120 string.to_s.split(',').map do |pair|
  103. 781 pair.split('-').map(&:to_i)
  104. end
  105. end
  106. 1 def self.serialize(ranges)
  107. 3107 ranges.map{|r| [r.first,r.last].join('-')}.join(',')
  108. end
  109. 1 def self.reduce(ranges)
  110. # or ranges[0][0].nil? is a regert: https://bugs.loomio.io/organizations/loomio/issues/499/
  111. 3012 return [] if ranges.length == 0 or ranges[0][0].nil?
  112. 8515 ranges = ranges.sort_by {|r| r.first }
  113. 2637 reduced = [ranges.shift]
  114. 2637 ranges.each do |r|
  115. 3241 lastr = reduced[-1]
  116. 3241 if lastr.last >= r.first - 1
  117. 1011 reduced[-1] = [lastr.first,[r.last, lastr.last].max]
  118. else
  119. 2230 reduced.push(r)
  120. end
  121. end
  122. 2637 reduced
  123. end
  124. end

app/extras/time_zone_to_city.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 class TimeZoneToCity
  2. 1 def self.convert(iana_name)
  3. 207 city_guess = iana_name.split('/')[1]
  4. 207 if city_list.include? city_guess
  5. 1 city_guess
  6. else
  7. 206 offset = offset_for_iana(iana_name)
  8. 206 city_from_offset(offset)
  9. end
  10. end
  11. 1 def self.city_list
  12. 413 ActiveSupport::TimeZone::MAPPING.keys
  13. end
  14. 1 def self.city_from_offset(offset)
  15. 206 offsets_with_city[offset]
  16. end
  17. 1 def self.offset_for_iana(iana_name)
  18. 206 ActiveSupport::TimeZone[iana_name].try(:formatted_offset) || "+00:00"
  19. end
  20. 1 def self.offsets_with_city
  21. 31312 a = city_list.map { |city| [ActiveSupport::TimeZone[city].formatted_offset, city] }.flatten
  22. 206 Hash[*a]
  23. end
  24. end

app/extras/user_inviter.rb

94.55% lines covered

55 relevant lines. 52 lines covered and 3 lines missed.
    
  1. 1 class UserInviter
  2. 1 def self.count(emails: , user_ids:, chatbot_ids:, audience:, model:, usernames: , actor:, exclude_members: false, include_actor: false)
  3. 2 emails = Array(emails).map(&:presence).compact.uniq
  4. 2 user_ids = Array(user_ids).uniq.compact.map(&:to_i)
  5. 2 chatbot_ids = Array(chatbot_ids).uniq.compact.map(&:to_i)
  6. 2 usernames = Array(usernames).map(&:presence).compact.uniq
  7. 2 audience_ids = AnnouncementService.audience_users(
  8. model, audience, actor, exclude_members, include_actor).pluck(:id)
  9. 2 email_count = emails.count - User.where(email: emails).count
  10. 2 users = User.active.where('email in (:emails) or id in (:user_ids) or username IN (:usernames)',
  11. emails: emails,
  12. usernames: usernames,
  13. user_ids: user_ids.concat(audience_ids))
  14. 2 users = users.where.not(id: model.voter_ids) if exclude_members
  15. 2 email_count + users.count + chatbot_ids.length
  16. end
  17. 1 def self.authorize_add_members!(parent_group:, group_ids:, emails:, user_ids:, actor: )
  18. 17 subscription = Subscription.for(parent_group)
  19. 17 raise Subscription::NotActive unless subscription.is_active?
  20. # authorize ability to add members to selected groups
  21. 17 Group.where(id: group_ids).each do |g|
  22. 18 actor.ability.authorize!(:add_members, g)
  23. end
  24. 16 return if subscription.max_members.nil?
  25. 3 new_count = new_members_count(parent_group: parent_group, user_ids: user_ids, emails: emails)
  26. 3 if (parent_group.org_members_count + new_count) > parent_group.subscription.max_members.to_i
  27. 3 raise Subscription::MaxMembersExceeded
  28. end
  29. end
  30. # how many totally new members are being added right now?
  31. 1 def self.new_members_count(parent_group:, user_ids:, emails:)
  32. new_emails_count =
  33. 3 emails.uniq.count -
  34. Membership.active.where(
  35. group_id: parent_group.id_and_subgroup_ids,
  36. user_id: User.where(email: emails).pluck(:id),
  37. ).count
  38. new_user_ids_count =
  39. 3 user_ids.uniq.count -
  40. Membership.active.where(
  41. group_id: parent_group.id_and_subgroup_ids,
  42. user_id: user_ids,
  43. ).count
  44. 3 new_emails_count + new_user_ids_count
  45. end
  46. 1 def self.authorize!(emails: , user_ids:, audience:, model:, actor:)
  47. # check inviter can notify group if that's happening
  48. # check inviter can invite guests (from the org, or external) if that's happening
  49. 490 user_ids = Array(user_ids).uniq.compact.map(&:to_i)
  50. 490 emails = Array(emails).map(&:presence).compact.uniq
  51. # members belong to group
  52. 490 member_ids = model.members.where(id: user_ids).pluck(:id)
  53. 490 member_ids += model.members.where(email: emails).pluck(:id)
  54. 490 emails -= User.where(email: emails, id: member_ids).pluck(:email)
  55. # guests are outside of the group, but allowed to be referenced by user query
  56. 490 guest_ids = UserQuery.invitable_user_ids(model: model, actor: actor, user_ids: user_ids - member_ids)
  57. 490 actor.ability.authorize!(:announce, model) if audience == 'group'
  58. 487 actor.ability.authorize!(:add_members, model) if member_ids.any?
  59. 487 actor.ability.authorize!(:add_guests, model) if emails.any? or guest_ids.any?
  60. end
  61. 1 def self.where_existing(user_ids:, audience:, model:, actor:)
  62. user_ids = Array(user_ids).uniq.compact.map(&:to_i)
  63. audience_ids = AnnouncementService.audience_users(model, audience, actor).pluck(:id)
  64. model.members.where('users.id': user_ids + audience_ids)
  65. end
  66. 1 def self.where_or_create!(emails:, user_ids:, audience: nil, model:, actor:, include_actor: false)
  67. 1214 user_ids = Array(user_ids).uniq.compact.map(&:to_i)
  68. 1214 emails = Array(emails).uniq.compact
  69. 1214 audience_ids = if audience
  70. 180 AnnouncementService.audience_users(model, audience, actor, false, include_actor).pluck(:id)
  71. else
  72. 1034 []
  73. end
  74. # guests are any user outside of the group, and not yet invited
  75. # either by email address or by user_id, but user_ids are limited to your org
  76. 1214 member_ids = model.members.where(id: user_ids).pluck(:id)
  77. # guests are outside of the group, but allowed to be referenced by user query
  78. 1214 guest_ids = UserQuery.invitable_user_ids(model: model, actor: actor, user_ids: user_ids - member_ids)
  79. 1214 ids = member_ids.concat(guest_ids).concat(audience_ids).uniq
  80. 1214 ThrottleService.limit!(key: 'UserInviterInvitations',
  81. id: actor.id,
  82. max: actor.invitations_rate_limit,
  83. inc: emails.length + ids.length,
  84. per: :day)
  85. 1214 wday = Date.today.wday
  86. 1214 User.import(safe_emails(emails).map do |email|
  87. 38 User.new(email: email,
  88. time_zone: actor.time_zone,
  89. date_time_pref: actor.date_time_pref,
  90. detected_locale: actor.locale,
  91. email_catch_up_day: wday)
  92. end, on_duplicate_key_ignore: true)
  93. 1214 User.active.where("id in (:ids) or email in (:emails)", ids: ids, emails: emails)
  94. end
  95. 1 private
  96. 1 def self.safe_emails(emails)
  97. 1252 emails.uniq.reject {|email| NoSpam::SPAM_REGEX.match?(email) }
  98. end
  99. end

app/extras/username_generator.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. 1 class UsernameGenerator
  2. 1 attr_reader :user
  3. 1 def initialize(user)
  4. 6694 @user = user
  5. end
  6. 1 def generate
  7. 6694 return safe_username unless conflict_exists?(safe_username)
  8. 110 low = 1
  9. 110 high = 1
  10. 110 begin
  11. 192 username = "#{safe_username}#{(low..high).to_a.sample}"
  12. 192 high = high * 2
  13. end while conflict_exists?(username)
  14. 110 username
  15. end
  16. 1 private
  17. 1 def base_username
  18. 13470 if user.name.present?
  19. 13366 user.name
  20. 104 elsif user.email.present?
  21. 104 user.email.split('@').first
  22. else
  23. 'unknownuser'
  24. end
  25. end
  26. 1 def safe_username
  27. 13470 ActiveSupport::Inflector.transliterate(base_username)
  28. .downcase
  29. .gsub(/[^a-z0-9]+/, '')[0,18]
  30. end
  31. 1 def conflict_exists?(username)
  32. 6886 User.where(username: username).exists?
  33. end
  34. end

app/helpers/application_helper.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. module ApplicationHelper
  2. def vue_css_includes
  3. vue_index = File.read(Rails.root.join('public/blient/index.html'))
  4. Nokogiri::HTML(vue_index).css('head link[as=style], head link[rel=stylesheet]').to_s
  5. end
  6. def vue_js_includes
  7. vue_index = File.read(Rails.root.join('public/blient/index.html'))
  8. Nokogiri::HTML(vue_index).css('head link[as=script], script').to_s
  9. end
  10. def metadata
  11. @metadata ||= if should_have_metadata && current_user.can?(:show, resource)
  12. "Metadata::#{controller_name.singularize.camelize}Serializer".constantize.new(resource)
  13. else
  14. {title: AppConfig.theme[:site_name], image_urls: []}
  15. end.as_json
  16. end
  17. def resource
  18. ModelLocator.new(resource_name, params).locate
  19. end
  20. def assign_resource
  21. instance_variable_get("@#{resource_name}") ||
  22. instance_variable_set("@#{resource_name}", resource)
  23. end
  24. def resource_name
  25. controller_name.singularize
  26. end
  27. def should_have_metadata
  28. %w{discussion group poll user}.include? controller_name.singularize.downcase
  29. end
  30. end

app/helpers/current_user_helper.rb

0.0% lines covered

28 relevant lines. 0 lines covered and 28 lines missed.
    
  1. module CurrentUserHelper
  2. include PendingActionsHelper
  3. class SpamUserDeniedError < StandardError
  4. end
  5. def sign_in(user)
  6. @current_user = nil
  7. user = UserService.verify(user: user)
  8. super(user) && handle_pending_actions(user) && associate_user_to_visit
  9. end
  10. def current_user
  11. @current_user || super || LoggedOutUser.new(locale: logged_out_preferred_locale, params: params, session: session)
  12. end
  13. def deny_spam_users
  14. if NoSpam::SPAM_REGEX.match?(current_user.email)
  15. raise SpamUserDeniedError.new(current_user.email)
  16. end
  17. end
  18. def require_current_user
  19. respond_with_error(status: 401) unless current_user && current_user.is_logged_in?
  20. end
  21. private
  22. def restricted_user
  23. User.find_by!(params.slice(:unsubscribe_token).permit!).tap { |user| user.restricted = true } if params[:unsubscribe_token]
  24. end
  25. def set_last_seen_at
  26. current_user.update_attribute :last_seen_at, Time.now
  27. end
  28. end

app/helpers/dev/dashboard_helper.rb

0.0% lines covered

32 relevant lines. 0 lines covered and 32 lines missed.
    
  1. module Dev::DashboardHelper
  2. private
  3. def pinned_discussion
  4. create_discussion!(:pinned_discussion) { |discussion| pin!(discussion) }
  5. end
  6. def poll_discussion
  7. create_discussion!(:poll_discussion, group: create_poll_group) { |discussion| add_poll!(discussion) }
  8. end
  9. def recent_discussion(group: create_group)
  10. create_discussion!(:recent_discussion, group: group)
  11. end
  12. def old_discussion
  13. create_discussion!(:old_discussion) { |discussion| discussion.update last_activity_at: 2.years.ago }
  14. end
  15. def create_discussion!(name, group: create_group, author: patrick)
  16. var_name = :"@#{name}"
  17. if existing = instance_variable_get(var_name)
  18. existing
  19. else
  20. instance_variable_set(var_name, Discussion.create!(title: name.to_s.humanize, group: group, author: author, private: false).tap do |discussion|
  21. DiscussionService.create(discussion: discussion, actor: discussion.author)
  22. yield discussion if block_given?
  23. end)
  24. end
  25. end
  26. def pin!(discussion)
  27. DiscussionService.pin(discussion: discussion, actor: discussion.author)
  28. end
  29. def add_poll!(discussion, name: 'Test poll', actor: jennifer)
  30. PollService.create(poll: Poll.new(poll_type: :poll, poll_option_names: ["Apple", "Banana"], title: name, closing_at: 3.days.from_now, discussion: discussion), actor: actor)
  31. end
  32. end

app/helpers/dev/fake_data_helper.rb

0.0% lines covered

387 relevant lines. 0 lines covered and 387 lines missed.
    
  1. module Dev::FakeDataHelper
  2. private
  3. def saved(obj)
  4. obj.tap(&:save!)
  5. end
  6. # only return new'd objects
  7. def fake_user(args = {})
  8. u = User.new({
  9. name: [Faker::Name.name,
  10. Faker::TvShows::RuPaul.queen,
  11. Faker::Superhero.name,
  12. Faker::TvShows::BojackHorseman.character,
  13. Faker::Movies::BackToTheFuture.character].sample.truncate(100),
  14. email: Faker::Internet.email,
  15. password: 'loginlogin',
  16. detected_locale: 'en',
  17. email_verified: true,
  18. date_time_pref: 'day_abbr',
  19. legal_accepted: true,
  20. experiences: {changePicture: true}
  21. }.merge(args))
  22. # # u.attach io: open(Faker::Avatar.image)
  23. # u.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/patrick.png"), filename: 'patrick.jpg'
  24. # u.update(avatar_kind: :uploaded)
  25. u
  26. end
  27. def fake_unverified_user(args = {})
  28. User.new({
  29. email: Faker::Internet.email,
  30. email_verified: false,
  31. }.merge(args))
  32. end
  33. def fake_group(args = {})
  34. defaults = {
  35. name: Faker::Company.name,
  36. description: [
  37. Faker::TvShows::BojackHorseman.quote,
  38. Faker::Movies::BackToTheFuture.quote].sample
  39. }
  40. values = defaults.merge(args)
  41. values[:handle] = values[:name].parameterize
  42. group = Group.new(values)
  43. # group.tags = [fake_tag]
  44. # puts 'attaching'
  45. # group.logo.attach(
  46. # io: URI.open(Rails.root.join('public/brand/icon_sky_300h.png')),
  47. # filename: 'loomiologo.png',
  48. # identify: false,
  49. # content_type: 'image/png'
  50. # )
  51. # puts 'attached'
  52. # group.cover_photo.attach(io: URI.open(Rails.root.join('public/brand/logo_sky_256h.png')), filename: 'loomiocover.png')
  53. group
  54. end
  55. def fake_tag(args = {})
  56. defaults = {
  57. name: Faker::Space.planet,
  58. color: Faker::Color.hex_color
  59. }
  60. Tag.new(defaults.merge(args))
  61. end
  62. def fake_discussion(args = {})
  63. Discussion.new({
  64. title: [Faker::TvShows::BojackHorseman.tongue_twister,
  65. Faker::TvShows::Friends.quote,
  66. Faker::Quote.yoda,
  67. Faker::Quote.robin].sample.truncate(150),
  68. description: [Faker::TvShows::BojackHorseman.quote,
  69. Faker::TvShows::Simpsons.quote,
  70. Faker::Quote.famous_last_words].sample,
  71. private: true,
  72. tags: ['spicy'],
  73. group: fake_group,
  74. author: fake_user}.merge(args))
  75. end
  76. def fake_new_comment_event(comment = fake_comment)
  77. Events::NewComment.new(
  78. user: comment.author,
  79. kind: 'new_comment',
  80. eventable: comment,
  81. discussion: comment.discussion
  82. )
  83. end
  84. def fake_new_discussion_event(discussion = fake_discussion)
  85. Events::NewDiscussion.new(
  86. user: discussion.author,
  87. kind: 'new_discussion',
  88. eventable: discussion
  89. )
  90. end
  91. def fake_poll_created_event(poll = fake_poll)
  92. Events::PollCreated.new(
  93. user: poll.author,
  94. kind: 'poll_created',
  95. eventable: poll,
  96. discussion: poll.discussion
  97. )
  98. end
  99. def fake_stance_created_event(stance = fake_stance)
  100. Events::StanceCreated.new(
  101. user_id: stance[:participant_id],
  102. kind: 'stance_created',
  103. eventable: stance,
  104. discussion: stance.poll.discussion
  105. )
  106. end
  107. def fake_outcome_created_event(outcome = fake_outcome)
  108. Events::OutcomeCreated.new(
  109. user_id: outcome.author_id,
  110. kind: 'outcome_created',
  111. eventable: outcome,
  112. discussion: outcome.discussion
  113. )
  114. end
  115. def fake_membership(args = {})
  116. Membership.new({
  117. group: fake_group,
  118. user: fake_user,
  119. }.merge(args))
  120. end
  121. def fake_membership_request(args = {})
  122. MembershipRequest.new({
  123. requestor: fake_user,
  124. group: fake_group
  125. }.merge(args))
  126. end
  127. def fake_identity(args = {})
  128. Identities::Base.new({
  129. user: fake_user,
  130. uid: "abc",
  131. access_token: SecureRandom.uuid,
  132. identity_type: :slack
  133. }.merge(args))
  134. end
  135. def option_names(option_count)
  136. seed = (0..20).to_a.sample
  137. options = option_count.times.map do
  138. [
  139. Faker::Food.ingredient,
  140. Faker::Movies::StarWars.call_squadron
  141. ].sample.truncate(250)
  142. end.uniq
  143. {
  144. poll: options,
  145. proposal: %w[agree abstain disagree block],
  146. count: %w[accept decline],
  147. check: %w[looks_good not_sure concerned],
  148. dot_vote: options,
  149. meeting: option_count.times.map { |i| (seed+i).days.from_now.iso8601},
  150. ranked_choice: options,
  151. score: options
  152. }.with_indifferent_access
  153. end
  154. def fake_poll(args = {})
  155. names = option_names(args.delete(:option_count) || (2..7).to_a.sample)
  156. closing_at = args[:wip] ? nil : 3.days.from_now
  157. options = {
  158. author: fake_user,
  159. discussion: fake_discussion,
  160. poll_type: 'poll',
  161. title: [Faker::Superhero.name, Faker::Movies::StarWars.quote].sample.truncate(140),
  162. tags: ['biggin'],
  163. details: [
  164. Faker::Movies::StarWars.quote,
  165. Faker::Movies::HitchhikersGuideToTheGalaxy.marvin_quote,
  166. Faker::Movies::PrincessBride.quote,
  167. Faker::Movies::Lebowski.quote,
  168. Faker::Movies::HitchhikersGuideToTheGalaxy.quote].sample,
  169. poll_option_names: names[args.fetch(:poll_type, :poll)],
  170. closing_at: closing_at,
  171. specified_voters_only: false,
  172. custom_fields: {}
  173. }.merge args.tap {|a| a.delete(:wip)}
  174. case options[:poll_type].to_s
  175. when 'dot_vote'
  176. options[:dots_per_person] = 10
  177. when 'meeting'
  178. options[:time_zone] = 'Asia/Seoul'
  179. options[:can_respond_maybe] = true
  180. when 'ranked_choice'
  181. options[:minimum_stance_choices] = 3
  182. when 'score'
  183. options[:max_score] = 9
  184. options[:min_score] = -9
  185. end
  186. Poll.new(options)
  187. end
  188. def create_fake_stances(poll:)
  189. (2..7).to_a.sample.times do
  190. u = fake_user
  191. poll.group.add_member!(u) if poll.group
  192. stance = fake_stance(poll: poll)
  193. stance.save!
  194. stance.create_missing_created_event!
  195. end
  196. poll.update_counts!
  197. end
  198. def fake_score(poll, index = 0)
  199. case poll.poll_type
  200. when 'score'
  201. ((poll.min_score)..(poll.max_score)).to_a.sample
  202. when 'ranked_choice'
  203. index + 1
  204. when 'meeting'
  205. if poll.can_respond_maybe
  206. [0,1,2].sample
  207. else
  208. [0,2].sample
  209. end
  210. else
  211. 1
  212. end
  213. end
  214. def cast_stance_params(poll)
  215. if poll.require_all_choices
  216. num_choices = poll.poll_options.length
  217. else
  218. num_choices = (poll.minimum_stance_choices..poll.maximum_stance_choices).to_a.sample
  219. end
  220. choice = poll.poll_options.sample(num_choices).map.with_index do |option, index|
  221. score = fake_score(poll)
  222. [option.name, fake_score(poll, index)]
  223. end.to_h
  224. reason = [
  225. Faker::Hipster.sentence,
  226. Faker::GreekPhilosophers.quote,
  227. Faker::TvShows::RuPaul.quote,
  228. ""
  229. ].sample
  230. {
  231. choice: choice,
  232. reason: reason
  233. }
  234. end
  235. def fake_stance(args = {})
  236. poll = args[:poll] || saved(fake_poll)
  237. if poll.require_all_choices
  238. num_choices = poll.poll_options.length
  239. else
  240. num_choices = (poll.minimum_stance_choices..poll.maximum_stance_choices).to_a.sample
  241. end
  242. choice = poll.poll_options.sample(num_choices).map.with_index do |option, index|
  243. score = fake_score(poll)
  244. [option.name, fake_score(poll, index)]
  245. end.to_h
  246. Stance.new({
  247. poll: poll,
  248. participant: fake_user,
  249. reason: [
  250. Faker::Hipster.sentence,
  251. Faker::GreekPhilosophers.quote,
  252. Faker::TvShows::RuPaul.quote,
  253. ""].sample,
  254. choice: choice
  255. }.merge(args))
  256. end
  257. def fake_comment(args = {})
  258. Comment.new({
  259. discussion: fake_discussion,
  260. body: Faker::ChuckNorris.fact,
  261. author: fake_user
  262. }.merge(args))
  263. end
  264. def fake_reaction(args = {})
  265. Reaction.new({
  266. reactable: fake_comment,
  267. user: fake_user,
  268. reaction: "+1"
  269. }.merge(args))
  270. end
  271. def fake_outcome(args = {})
  272. poll = fake_poll
  273. Outcome.new({
  274. poll: poll,
  275. author: poll.author,
  276. statement: with_markdown(Faker::Hipster.sentence)
  277. }.merge(args))
  278. end
  279. def fake_received_email(args = {})
  280. ReceivedEmail.new({
  281. sender_email: Faker::Internet.email,
  282. subject: Faker::ChuckNorris.fact,
  283. body: "FORWARDED MESSAGE------ TO: Mary <mary@example.com>, beth@example.com, Tim <tim@example.com> SUBJECT: We're having an argument! blahblahblah",
  284. })
  285. end
  286. def create_group_with_members
  287. group = saved(fake_group)
  288. group.add_admin!(saved(fake_user))
  289. (7..9).to_a.sample.times do
  290. group.add_member!(saved(fake_user))
  291. end
  292. create_chatbots_for_group(group)
  293. group
  294. end
  295. def create_chatbots_for_group(group)
  296. event_kinds = %w[
  297. new_discussion
  298. discussion_edited
  299. poll_created
  300. poll_edited
  301. poll_closing_soon
  302. poll_expired
  303. poll_announced
  304. poll_reopened
  305. outcome_created
  306. ]
  307. if ENV['TEST_MATRIX_SERVER']
  308. Chatbot.create!(
  309. group: group,
  310. kind: "matrix",
  311. server: ENV['TEST_MATRIX_SERVER'],
  312. channel: ENV['TEST_MATRIX_CHANNEL'],
  313. access_token: ENV['TEST_MATRIX_ACCESS_TOKEN'],
  314. event_kinds: event_kinds,
  315. # notification_only: true,
  316. name: "Matrix"
  317. )
  318. end
  319. if ENV['TEST_TEAMS_WEBHOOK']
  320. Chatbot.create!(
  321. group: group,
  322. kind: "webhook",
  323. webhook_kind: "microsoft",
  324. server: ENV['TEST_TEAMS_WEBHOOK'],
  325. event_kinds: event_kinds,
  326. # notification_only: true,
  327. name: "Microsoft Teams"
  328. )
  329. end
  330. if ENV['TEST_SLACK_WEBHOOK']
  331. Chatbot.create!(
  332. group: group,
  333. kind: "webhook",
  334. webhook_kind: "slack",
  335. server: ENV['TEST_SLACK_WEBHOOK'],
  336. event_kinds: event_kinds,
  337. # notification_only: true,
  338. name: "Slack"
  339. )
  340. end
  341. if ENV['TEST_DISCORD_WEBHOOK']
  342. Chatbot.create!(
  343. group: group,
  344. kind: "webhook",
  345. webhook_kind: "discord",
  346. server: ENV['TEST_DISCORD_WEBHOOK'],
  347. event_kinds: event_kinds,
  348. # notification_only: true,
  349. name: "Discord"
  350. )
  351. end
  352. end
  353. def create_fake_poll_in_group(args = {})
  354. saved(build_fake_poll_in_group)
  355. end
  356. def create_discussion_with_nested_comments
  357. group = create_group_with_members
  358. group.reload
  359. discussion = saved fake_discussion(group: group)
  360. DiscussionService.create(discussion: discussion, actor: group.admins.first)
  361. 15.times do
  362. parent_author = fake_user
  363. group.add_member! parent_author
  364. parent = fake_comment(discussion: discussion)
  365. CommentService.create(comment: parent, actor: parent_author)
  366. (0..3).to_a.sample.times do
  367. reply_author = fake_user
  368. group.add_member! reply_author
  369. reply = fake_comment(discussion: discussion, parent: parent)
  370. CommentService.create(comment: reply, actor: reply_author)
  371. end
  372. end
  373. discussion.reload
  374. EventService.repair_thread(discussion.id)
  375. discussion.reload
  376. end
  377. def create_discussion_with_sampled_comments
  378. group = create_group_with_members
  379. discussion = saved fake_discussion(group: group)
  380. DiscussionService.create(discussion: discussion, actor: group.admins.first)
  381. discussion.update(max_depth: 3)
  382. 5.times do
  383. group.add_member! saved(fake_user)
  384. end
  385. 10.times do
  386. CommentService.create(comment: fake_comment(discussion: discussion), actor: group.members.sample)
  387. end
  388. comments = discussion.reload.comments
  389. 10.times do
  390. CommentService.create(comment: fake_comment(discussion: discussion, parent: comments.sample), actor: group.members.sample)
  391. end
  392. comments = discussion.reload.comments
  393. 10.times do
  394. CommentService.create(comment: fake_comment(discussion: discussion, parent: comments.sample), actor: group.members.sample)
  395. end
  396. discussion.reload
  397. EventService.repair_thread(discussion.id)
  398. discussion.reload
  399. discussion
  400. end
  401. private
  402. def with_markdown(text)
  403. "#{text} - **(markdown!)**"
  404. end
  405. end

app/helpers/dev/ninties_movies_helper.rb

0.0% lines covered

318 relevant lines. 0 lines covered and 318 lines missed.
    
  1. module Dev::NintiesMoviesHelper
  2. include Dev::FakeDataHelper
  3. private
  4. # try to just return objects here. Don't knit them together. Leave that for
  5. # the development controller action to do if possible
  6. def patrick
  7. @patrick ||= User.find_by(email: 'patrick_swayze@example.com') ||
  8. User.create!(name: 'Patrick Swayze',
  9. email: 'patrick_swayze@example.com',
  10. is_admin: false,
  11. username: 'patrickswayze',
  12. password: 'gh0stmovie',
  13. experiences: {changePicture: true},
  14. detected_locale: 'en',
  15. date_time_pref: 'day_abbr',
  16. avatar_kind: 'uploaded',
  17. email_verified: true)
  18. @patrick.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/patrick.png"), filename: 'patrick.jpg'
  19. @patrick.update(avatar_kind: :uploaded)
  20. @patrick
  21. end
  22. def patricks_contact
  23. if patrick.contacts.empty?
  24. patrick.contacts.create(name: 'Keanu Reeves',
  25. email: 'keanu@example.com',
  26. date_time_pref: 'day_abbr',
  27. source: 'gmail')
  28. end
  29. end
  30. def jennifer
  31. @jennifer ||= User.find_by(email: 'jennifer_grey@example.com') ||
  32. User.create!(name: 'Jennifer Grey',
  33. email: 'jennifer_grey@example.com',
  34. date_time_pref: 'day_abbr',
  35. username: 'jennifergrey',
  36. experiences: {changePicture: true},
  37. email_verified: true)
  38. @jennifer.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/jennifer.png"), filename: 'jen.jpg'
  39. @jennifer.update(avatar_kind: :uploaded)
  40. @jennifer
  41. end
  42. def max
  43. @max ||= User.find_by(email: 'max@example.com') ||
  44. User.create!(name: 'Max Von Sydow',
  45. email: 'max@example.com',
  46. password: 'gh0stmovie',
  47. username: 'mingthemerciless',
  48. date_time_pref: 'day_abbr',
  49. email_verified: true)
  50. @max
  51. end
  52. def emilio
  53. @emilio ||= User.find_by(email: 'emilio@loomio.org') ||
  54. User.create!(name: 'Emilio Estevez',
  55. email: 'emilio@loomio.org',
  56. password: 'gh0stmovie',
  57. date_time_pref: 'day_abbr',
  58. email_verified: true)
  59. end
  60. def judd
  61. @judd ||= User.find_by(email: 'judd@example.com') ||
  62. User.create!(name: 'Judd Nelson',
  63. email: 'judd@example.com',
  64. password: 'gh0stmovie',
  65. date_time_pref: 'day_abbr',
  66. email_verified: true)
  67. end
  68. def rudd
  69. @rudd ||= User.find_by(email: 'rudd@example.com') ||
  70. User.create!(name: 'Paul Rudd',
  71. email: 'rudd@example.com',
  72. password: 'gh0stmovie',
  73. date_time_pref: 'day_abbr',
  74. email_verified: true)
  75. end
  76. def create_group
  77. unless @group
  78. @group = Group.new(name: 'Dirty Dancing Shoes',
  79. description: 'The best place for dancing shoes. _every_ shoe is **dirty**!',
  80. group_privacy: 'closed',
  81. handle: 'shoes',
  82. discussion_privacy_options: 'public_or_private', creator: patrick)
  83. file = open(Rails.root.join('public','brand','icon_sky_150h.png'))
  84. @group.logo.attach(io: file, filename: 'logo.png')
  85. GroupService.create(group: @group, actor: @group.creator)
  86. @group.add_admin! patrick
  87. @group.add_member! jennifer
  88. @group.add_member! emilio
  89. end
  90. @group
  91. end
  92. def create_poll_group
  93. unless @poll_group
  94. @poll_group = Group.new(name: 'Dirty Dancing Shoes',
  95. group_privacy: 'closed',
  96. discussion_privacy_options: 'public_or_private',
  97. creator: patrick)
  98. GroupService.create(group: @poll_group, actor: @poll_group.creator)
  99. @poll_group.add_admin! patrick
  100. @poll_group.add_member! jennifer
  101. @poll_group.add_member! emilio
  102. end
  103. @poll_group
  104. end
  105. def multiple_groups
  106. @groups = []
  107. 10.times do
  108. group = Group.new(name: Faker::Name.name,
  109. group_privacy: 'closed',
  110. discussion_privacy_options: 'public_or_private', creator: patrick)
  111. group.add_admin! patrick
  112. GroupService.create(group: group, actor: group.creator)
  113. @groups << group
  114. end
  115. @groups
  116. end
  117. def muted_create_group
  118. unless @muted_group
  119. @muted_group = Group.new(name: 'Muted Point Blank',
  120. group_privacy: 'closed',
  121. discussion_privacy_options: 'public_or_private', creator: patrick)
  122. GroupService.create(group: @muted_group, actor: @muted_group.creator)
  123. @muted_group.add_admin! patrick
  124. Membership.find_by(group: @muted_group, user: patrick).set_volume! :mute
  125. end
  126. @muted_group
  127. end
  128. def create_another_group
  129. unless @another_group
  130. @another_group = Group.new(name: 'Point Break',
  131. group_privacy: 'closed',
  132. discussion_privacy_options: 'public_or_private',
  133. description: 'An FBI agent goes undercover to catch a gang of bank robbers who may be surfers.', creator: patrick)
  134. GroupService.create(group: @another_group, actor: @another_group.creator)
  135. @another_group.add_admin! patrick
  136. @another_group.add_member! max
  137. end
  138. @another_group
  139. end
  140. def create_discussion
  141. unless @discussion
  142. @discussion = Discussion.create(title: 'What star sign are you?', private: false, group: create_group, link_previews: [{'title': 'link title', 'url': 'https://www.example.com', 'description': 'a link to a page', 'image': 'https://www.loomio.org/theme/logo.svg', 'hostname':'www.example.com'}], author: jennifer)
  143. DiscussionService.create(discussion: @discussion, actor: @discussion.author)
  144. end
  145. @discussion
  146. end
  147. def create_another_discussion
  148. unless @another_discussion
  149. @another_discussion = Discussion.create(title: 'Waking Up in Reno',
  150. private: false,
  151. group: create_group,
  152. author: jennifer)
  153. DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
  154. end
  155. @another_discussion
  156. end
  157. def create_closed_discussion
  158. unless @closed_discussion
  159. @closed_discussion = Discussion.create(title: 'This thread is old and closed',
  160. private: false,
  161. closed_at: Time.now,
  162. group: create_group,
  163. author: jennifer)
  164. DiscussionService.create(discussion: @closed_discussion, actor: @closed_discussion.author)
  165. end
  166. @closed_discussion
  167. end
  168. def create_public_discussion
  169. unless @another_discussion
  170. @another_discussion = Discussion.create!(title: "The name's Johnny Utah!",
  171. private: false,
  172. group: create_another_group,
  173. author: patrick)
  174. DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
  175. end
  176. @another_discussion
  177. end
  178. def private_create_discussion
  179. unless @another_discussion
  180. @another_discussion = Discussion.create!(title: 'But are you crazy enough?',
  181. private: true,
  182. group: create_another_group,
  183. author: patrick)
  184. DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
  185. end
  186. @another_discussion
  187. end
  188. def create_subgroup
  189. unless @subgroup
  190. @subgroup = Group.new(name: 'Johnny Utah',
  191. parent: create_another_group,
  192. discussion_privacy_options: 'public_or_private',
  193. group_privacy: 'closed', creator: patrick)
  194. GroupService.create(group: @subgroup, actor: @subgroup.creator)
  195. discussion = FactoryBot.create :discussion, group: @subgroup, title: "Vaya con dios", private: false
  196. # discussion = @subgroup.discussions.create(title: "Vaya con dios", private: false, author: patrick)
  197. DiscussionService.create(discussion: discussion, actor: discussion.author)
  198. @subgroup.add_admin! patrick
  199. end
  200. @subgroup
  201. end
  202. def another_create_subgroup
  203. unless @another_subgroup
  204. @another_subgroup = Group.new(name: 'Bodhi',
  205. parent: create_another_group,
  206. group_privacy: 'closed',
  207. discussion_privacy_options: 'public_or_private',
  208. is_visible_to_parent_members: true, creator: patrick)
  209. GroupService.create(group: @another_subgroup, actor: @another_subgroup.creator)
  210. discussion = FactoryBot.create :discussion, group: @another_subgroup, title: "Vaya con dios 2", private: false
  211. DiscussionService.create(discussion: discussion, actor: discussion.author)
  212. @another_subgroup.add_admin! patrick
  213. end
  214. @another_subgroup
  215. end
  216. def pending_invitation
  217. @pending_membership ||= Membership.create(user: User.new(email: 'judd@example.com'),
  218. group: create_group, inviter: patrick)
  219. end
  220. def create_comment
  221. unless @create_comment
  222. @create_comment ||= Comment.create!(
  223. discussion: create_discussion,
  224. author: patrick,
  225. body: 'Hello world!'
  226. )
  227. end
  228. @create_comment
  229. end
  230. def create_poll
  231. @create_poll ||= Poll.create!(
  232. discussion: create_discussion,
  233. poll_type: :proposal,
  234. poll_option_names: %w(agree abstain disagree block),
  235. author: patrick,
  236. title: "Let's go to the moon!",
  237. closing_at: 10.days.from_now
  238. )
  239. end
  240. def create_stance
  241. @create_stance ||= Stance.create(
  242. poll: create_poll,
  243. participant: patrick,
  244. choice: :agree,
  245. reason: "I have unreasonably high expectations for how this will go!"
  246. )
  247. end
  248. def create_outcome
  249. @create_outcome ||= Outcome.create!(
  250. poll: create_poll.tap { |p| p.update(closed_at: 1.day.ago) },
  251. author: patrick,
  252. statement: "Okay let's do it!"
  253. )
  254. end
  255. def create_all_activity_items
  256. # discussion_edited
  257. create_discussion
  258. create_discussion.update(title: "another discussion title")
  259. Events::DiscussionEdited.publish!(discussion: create_discussion, actor: create_discussion.author)
  260. # discussion_moved
  261. Events::DiscussionMoved.publish!(create_discussion, patrick, create_another_group)
  262. # new_comment
  263. Events::NewComment.publish!(create_comment)
  264. # poll_created
  265. Events::PollCreated.publish!(create_poll, patrick)
  266. # poll_edited
  267. create_poll.update(title: "Another poll title")
  268. Events::PollEdited.publish!(poll: create_poll, actor: patrick)
  269. # stance_created
  270. Events::StanceCreated.publish!(create_stance)
  271. # poll_expired
  272. Events::PollExpired.publish!(create_poll)
  273. # poll_closed_by_user
  274. Events::PollClosedByUser.publish!(create_poll, patrick)
  275. # outcome_created
  276. Events::OutcomeCreated.publish!(outcome: create_outcome)
  277. end
  278. def create_all_notifications
  279. #'reaction_created'
  280. patrick_comment = Comment.new(discussion: create_discussion, body: 'I\'m rather likeable')
  281. reaction = Reaction.new(reactable: patrick_comment, reaction: ":heart:")
  282. new_comment_event = CommentService.create(comment: patrick_comment, actor: patrick)
  283. reaction_created_event = ReactionService.update(reaction: reaction, params: {reaction: ':slight_smile:'}, actor: jennifer)
  284. create_another_group.add_member! jennifer
  285. #'comment_replied_to'
  286. jennifer_comment = Comment.new(discussion: create_discussion,
  287. parent: patrick_comment,
  288. body: 'hey @patrickswayze you look great in that tuxeido (jen reply to patrick)')
  289. CommentService.create(comment: jennifer_comment, actor: jennifer)
  290. #'user_mentioned'
  291. reply_comment = Comment.new(discussion: create_discussion,
  292. body: 'I agree with @patrickswayze (jen mention patrick)', parent: jennifer_comment)
  293. CommentService.create(comment: reply_comment, actor: jennifer)
  294. [max, emilio, judd].each {|u| patrick_comment.group.add_member! u}
  295. ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':slight_smile:'}, actor: jennifer)
  296. ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':heart:'}, actor: patrick)
  297. ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':laughing:'}, actor: max)
  298. ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':cry:'}, actor: emilio)
  299. ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':wave:'}, actor: judd)
  300. #'membership_requested',
  301. membership_request = MembershipRequest.new(group: create_group)
  302. event = MembershipRequestService.create(membership_request: membership_request, actor: rudd)
  303. #'membership_request_approved',
  304. another_group = Group.new(name: 'Stars of the 90\'s', group_privacy: 'closed')
  305. GroupService.create(group: another_group, actor: jennifer)
  306. membership_request = MembershipRequest.new(requestor: patrick, group: another_group)
  307. event = MembershipRequestService.create(membership_request: membership_request, actor: patrick)
  308. approval_event = MembershipRequestService.approve(membership_request: membership_request, actor: jennifer)
  309. #'user_added_to_group',
  310. #notify patrick that he has been added to jens group
  311. another_group = Group.new(name: 'Planets of the 80\'s')
  312. GroupService.create(group: another_group, actor: jennifer)
  313. jennifer.reload
  314. GroupService.invite(group: another_group, params: { recipient_user_ids: [patrick.id] }, actor: jennifer)
  315. #'new_coordinator',
  316. #notify patrick that jennifer has made him a coordinator
  317. membership = Membership.find_by(user_id: patrick.id, group_id: another_group.id)
  318. new_coordinator_event = MembershipService.make_admin(membership: membership, actor: jennifer)
  319. #'invitation_accepted',
  320. #notify patrick that his invitation to emilio has been accepted
  321. membership = Membership.create(user: emilio, group: another_group, inviter: patrick)
  322. MembershipService.redeem(membership: membership, actor: emilio)
  323. poll = FactoryBot.create(:poll, discussion: create_discussion, group: create_group, author: jennifer, closing_at: 24.hours.from_now)
  324. PollService.invite(
  325. poll: poll,
  326. params: { recipient_user_ids: [patrick.id] },
  327. actor: jennifer
  328. )
  329. #'poll_closing_soon'
  330. PollService.publish_closing_soon
  331. #'outcome_created'
  332. poll = FactoryBot.build(:poll, discussion: create_discussion, author: jennifer, closed_at: 1.day.ago, closing_at: 1.day.ago)
  333. PollService.create(poll: poll, actor: jennifer)
  334. outcome = FactoryBot.build(:outcome, poll: poll)
  335. OutcomeService.create(
  336. outcome: outcome,
  337. params: {recipient_user_ids: [patrick.id]},
  338. actor: jennifer
  339. )
  340. #'stance_created'
  341. # notify patrick that someone has voted on his proposal
  342. poll = FactoryBot.build(:poll, closing_at: 4.days.from_now, discussion: create_discussion, voter_can_add_options: true)
  343. PollService.create(poll: poll, actor: patrick)
  344. end
  345. end

app/helpers/dev/scenarios_helper.rb

0.0% lines covered

279 relevant lines. 0 lines covered and 279 lines missed.
    
  1. module Dev::ScenariosHelper
  2. include Dev::FakeDataHelper
  3. def poll_created_scenario(params)
  4. group = create_group_with_members
  5. discussion = fake_discussion(group: group, title: "Some discussion")
  6. DiscussionService.create(discussion: discussion, actor: group.members.first)
  7. actor = group.admins.first
  8. user = saved(fake_user(time_zone: "America/New_York"))
  9. group.add_member! user if !params[:guest]
  10. group.add_admin! user if params[:admin]
  11. poll = fake_poll(group: group,
  12. discussion: params[:standalone] ? nil : discussion,
  13. poll_type: params[:poll_type],
  14. hide_results: (params[:hide_results] || :off),
  15. wip: params[:wip],
  16. anonymous: !!params[:anonymous])
  17. event = PollService.create(poll: poll, actor: actor, params: {notify_recipients: true})
  18. if params[:guest]
  19. recipients = {recipient_emails: [user.email], notify_recipients: true}
  20. PollService.invite(poll: poll, params: recipients, actor: actor)
  21. end
  22. {
  23. discussion: discussion,
  24. group: group,
  25. observer: user,
  26. poll: event.eventable,
  27. title: event.eventable.title,
  28. actor: actor,
  29. }
  30. end
  31. def poll_closed_scenario(params)
  32. observer = fake_user.tap(&:save!)
  33. group = create_group_with_members
  34. group.add_admin!(observer)
  35. poll = fake_poll(poll_type: params[:poll_type],
  36. anonymous: !!params[:anonymous],
  37. hide_results: (params[:hide_results] || :off),
  38. group: group,
  39. discussion: nil,
  40. wip: params[:wip])
  41. event = PollService.create(poll: poll, actor: observer)
  42. Stance.where(poll_id: poll.id, participant_id: observer.id).delete_all
  43. stance = fake_stance(poll: poll)
  44. StanceService.create(stance: stance, actor: observer)
  45. PollService.close(poll: poll, actor: observer)
  46. {
  47. observer: observer,
  48. group: group,
  49. actor: observer,
  50. title: event.eventable.title,
  51. poll: event.eventable
  52. }
  53. end
  54. def poll_user_mentioned_scenario(params)
  55. scenario = poll_created_scenario(params)
  56. voter = saved(fake_user)
  57. group_member = saved(fake_user)
  58. scenario[:poll].group.add_member!(voter)
  59. scenario[:poll].group.add_member!(group_member)
  60. stance = Stance.find_by(poll: scenario[:poll], participant: voter, latest: true)
  61. params = cast_stance_params(scenario[:poll])
  62. params[:reason] = "<p><span class='mention' data-mention-id='#{group_member.username}'>@#{group_member.name}</span></p>"
  63. params[:reason_format] = "html"
  64. StanceService.update(stance: stance, actor: voter, params: params)
  65. scenario[:actor] = voter
  66. scenario.merge(observer: group_member)
  67. end
  68. def poll_stance_created_scenario(params)
  69. scenario = poll_created_scenario(params)
  70. voter = saved(fake_user)
  71. scenario[:poll].group.add_member!(voter)
  72. Stance.where(poll_id: scenario[:poll].id,
  73. participant_id: scenario[:poll].author_id).update(volume: 'loud')
  74. stance = Stance.find_by(poll: scenario[:poll], participant: voter, latest: true)
  75. event = StanceService.update(stance: stance, actor: voter, params: cast_stance_params(scenario[:poll]))
  76. scenario[:stance] = event.eventable
  77. scenario[:actor] = event.eventable.participant
  78. scenario[:real_actor] = voter
  79. scenario.merge(observer: scenario[:poll].author, voter: voter)
  80. end
  81. def poll_anonymous_scenario(params)
  82. scenario = poll_created_scenario(params)
  83. voter = saved(fake_user)
  84. scenario[:poll].group.add_member!(voter)
  85. choices = [{poll_option_id: scenario[:poll].poll_option_ids[0]}]
  86. StanceService.create(stance: fake_stance(poll: scenario[:poll], stance_choices_attributes: choices), actor: voter)
  87. scenario[:actor] = voter
  88. scenario.merge(observer: scenario[:poll].author, voter: voter)
  89. end
  90. def poll_closing_soon_scenario(params)
  91. discussion = fake_discussion(group: create_group_with_members)
  92. non_voter = saved(fake_user)
  93. discussion.group.add_member! non_voter
  94. actor = discussion.group.admins.first
  95. DiscussionService.create(discussion: discussion, actor: actor)
  96. poll = fake_poll(
  97. author: actor,
  98. poll_type: params[:poll_type],
  99. anonymous: !!params[:anonymous],
  100. hide_results: (params[:hide_results] || :off),
  101. discussion: discussion,
  102. wip: params[:wip],
  103. notify_on_closing_soon: params[:notify_on_closing_soon] || 'voters',
  104. created_at: 6.days.ago,
  105. closing_at: if params[:wip] then nil else 1.day.from_now end
  106. )
  107. PollService.create(poll: poll, actor: actor)
  108. create_fake_stances(poll: poll)
  109. PollService.invite(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
  110. PollService.publish_closing_soon
  111. {
  112. discussion: discussion,
  113. group: discussion.group,
  114. observer: non_voter,
  115. actor: actor,
  116. poll: poll,
  117. title: poll.title
  118. }
  119. end
  120. def poll_reminder_scenario(params)
  121. discussion = fake_discussion(group: create_group_with_members)
  122. non_voter = saved(fake_user)
  123. discussion.group.add_member! non_voter
  124. actor = discussion.group.admins.first
  125. DiscussionService.create(discussion: discussion, actor: actor)
  126. poll = fake_poll(
  127. author: actor,
  128. poll_type: params[:poll_type],
  129. anonymous: !!params[:anonymous],
  130. hide_results: (params[:hide_results] || :off),
  131. discussion: discussion,
  132. wip: params[:wip],
  133. notify_on_closing_soon: params[:notify_on_closing_soon] || 'voters',
  134. created_at: 6.days.ago,
  135. closing_at: if params[:wip] then nil else 1.day.from_now end
  136. )
  137. PollService.create(poll: poll, actor: actor)
  138. create_fake_stances(poll:poll)
  139. # Stance.create(poll: poll, participant: non_voter)
  140. PollService.invite(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
  141. PollService.remind(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
  142. {
  143. discussion: discussion,
  144. group: discussion.group,
  145. observer: non_voter,
  146. actor: actor,
  147. poll: poll,
  148. title: poll.title
  149. }
  150. end
  151. def poll_closing_soon_author_scenario(params)
  152. params[:notify_on_closing_soon] = 'author'
  153. scenario = poll_closing_soon_scenario(params)
  154. scenario.merge(observer: scenario[:poll].author)
  155. end
  156. def poll_closing_soon_with_vote_scenario(params)
  157. discussion = fake_discussion(group: create_group_with_members)
  158. actor = discussion.group.admins.first
  159. poll = fake_poll(
  160. author: actor,
  161. poll_type: params[:poll_type],
  162. anonymous: !!params[:anonymous],
  163. hide_results: (params[:hide_results] || :off),
  164. notify_on_closing_soon: :voters,
  165. discussion: discussion,
  166. closing_at: if params[:wip] then nil else 1.day.from_now end
  167. )
  168. PollService.create(poll: poll, actor: actor)
  169. create_fake_stances(poll: poll)
  170. voter = poll.stances.last.real_participant
  171. discussion.add_guest! voter, discussion.author
  172. PollService.invite(poll: poll, params: {recipient_user_ids: [voter.id]}, actor: actor)
  173. PollService.publish_closing_soon
  174. {
  175. discussion: discussion,
  176. group: discussion.group,
  177. observer: voter,
  178. actor: actor,
  179. title: poll.title,
  180. poll: poll
  181. }
  182. end
  183. def poll_expired_scenario(params)
  184. scenario = poll_expired_author_scenario(params)
  185. scenario.merge(observer: scenario[:actor])
  186. end
  187. def poll_expired_author_scenario(params)
  188. discussion = fake_discussion(group: create_group_with_members)
  189. actor = discussion.group.admins.first
  190. params[:discussion] = discussion
  191. poll = fake_poll(
  192. discussion: discussion,
  193. poll_type: params[:poll_type],
  194. anonymous: !!params[:anonymous],
  195. hide_results: (params[:hide_results] || :off)
  196. )
  197. PollService.create(poll: poll, actor: actor)
  198. create_fake_stances(poll: poll)
  199. poll.update_attribute(:closing_at, 1.day.ago)
  200. poll.discussion.group.add_member! poll.author
  201. PollService.expire_lapsed_polls
  202. {
  203. discussion: discussion,
  204. group: discussion.group,
  205. actor: actor,
  206. observer: poll.author,
  207. title: poll.title,
  208. poll: poll
  209. }
  210. end
  211. def poll_outcome_created_scenario(params)
  212. discussion = saved(fake_discussion(group: create_group_with_members))
  213. actor = discussion.group.admins.first
  214. observer = fake_user
  215. discussion.group.add_member! observer
  216. poll = fake_poll(
  217. poll_type: params[:poll_type],
  218. anonymous: !!params[:anonymous],
  219. hide_results: (params[:hide_results] || :off),
  220. discussion: discussion,
  221. closed_at: 1.day.ago,
  222. closing_at: 1.day.ago
  223. )
  224. PollService.create(poll: poll, actor: actor)
  225. create_fake_stances(poll:poll)
  226. outcome = fake_outcome(poll: poll)
  227. OutcomeService.create(outcome: outcome, actor: actor, params: {recipient_emails: [observer.email]})
  228. { discussion: discussion,
  229. group: discussion.group,
  230. observer: observer,
  231. actor: actor,
  232. outcome: outcome,
  233. title: poll.title,
  234. poll: poll}
  235. end
  236. def poll_outcome_review_due_scenario(params)
  237. discussion = saved(fake_discussion(group: create_group_with_members))
  238. actor = discussion.group.admins.first
  239. observer = fake_user
  240. discussion.group.add_member! observer
  241. poll = fake_poll(
  242. poll_type: params[:poll_type],
  243. anonymous: !!params[:anonymous],
  244. hide_results: (params[:hide_results] || :off),
  245. discussion: discussion,
  246. closed_at: 1.day.ago,
  247. closing_at: 1.day.ago
  248. )
  249. PollService.create(poll: poll, actor: actor)
  250. create_fake_stances(poll: poll)
  251. outcome = fake_outcome(poll: poll, author: poll.author, review_on: Date.today)
  252. Events::OutcomeReviewDue.publish!(outcome)
  253. # OutcomeService.create(outcome: outcome, actor: actor, params: {recipient_emails: [observer.email]})
  254. { discussion: discussion,
  255. group: discussion.group,
  256. observer: poll.author,
  257. actor: actor,
  258. outcome: outcome,
  259. title: poll.title,
  260. poll: poll}
  261. end
  262. def poll_catch_up_scenario(params)
  263. discussion = saved(fake_discussion(group: create_group_with_members))
  264. scenario = poll_expired_scenario(params)
  265. observer = fake_user.tap(&:save!)
  266. observer.email_catch_up_day = 7
  267. discussion.group.add_member! observer
  268. scenario[:discussion].group.add_member! observer
  269. poll = scenario[:poll]
  270. choices = [{poll_option_id: poll.poll_option_ids[0]}]
  271. StanceService.create(stance: fake_stance(poll: poll, stance_choices_attributes: choices), actor: observer)
  272. UserMailer.catch_up(observer.id).deliver_now
  273. scenario.merge(observer: observer)
  274. end
  275. def alternative_poll_option_selection(poll_option_ids, i)
  276. poll_option_ids.each_with_index.map {|id, j| {poll_option_id: id, score: (i+j)%3}}
  277. end
  278. def saved(obj)
  279. obj.tap(&:save!)
  280. end
  281. end

app/helpers/email_helper.rb

0.0% lines covered

133 relevant lines. 0 lines covered and 133 lines missed.
    
  1. module EmailHelper
  2. include PrettyUrlHelper
  3. def login_token(recipient, redirect_path)
  4. recipient.login_tokens.create!(redirect: redirect_path)
  5. end
  6. def render_markdown(str, fmt = 'md')
  7. MarkdownService.render_markdown(str, fmt)
  8. end
  9. def render_plain_text(str, fmt = 'md')
  10. MarkdownService.render_plain_text(str, fmt)
  11. end
  12. def render_rich_text(str, fmt = 'md')
  13. MarkdownService.render_rich_text(str, fmt)
  14. end
  15. def recipient_stance(recipient, poll)
  16. poll.poll.stances.latest.find_by(participant: recipient) || Stance.new(poll: poll, participant: recipient)
  17. end
  18. def formatted_time_zone
  19. ActiveSupport::TimeZone[@recipient.time_zone].to_s
  20. end
  21. def tracked_url(model, args = {})
  22. args.merge!({utm_medium: 'email', utm_campaign: @event&.kind })
  23. if model.is_a?(Poll) or model.is_a?(Outcome)
  24. if stance = model.poll.stances.latest.find_by(participant: @recipient)
  25. args.merge!(stance_token: stance.token)
  26. end
  27. end
  28. if model.is_a?(Discussion) || model.is_a?(Comment)
  29. if reader = DiscussionReader.redeemable.find_by(user: @recipient, discussion: model.discussion)
  30. args.merge!(discussion_reader_token: reader.token)
  31. end
  32. end
  33. polymorphic_url(model, args)
  34. end
  35. def unfollow_url(discussion, action_name, recipient, new_volume: :quiet)
  36. email_actions_unfollow_discussion_url(
  37. discussion_id: discussion.id,
  38. utm_campaign: @event.kind,
  39. utm_medium: 'email',
  40. unsubscribe_token: @recipient.unsubscribe_token,
  41. new_volume: new_volume
  42. )
  43. end
  44. def preferences_url
  45. tracked_url(email_preferences_url(unsubscribe_token: @recipient.unsubscribe_token))
  46. end
  47. def pixel_src(event)
  48. email_actions_mark_discussion_as_read_url(
  49. discussion_id: event.eventable.discussion.id,
  50. event_id: event.id,
  51. unsubscribe_token: @recipient.unsubscribe_token,
  52. format: 'gif'
  53. )
  54. end
  55. def mark_notification_as_read_pixel_src(notification_id)
  56. email_actions_mark_notification_as_read_url(
  57. id: notification_id,
  58. unsubscribe_token: @recipient.unsubscribe_token,
  59. format: 'gif'
  60. )
  61. end
  62. def can_unfollow?(discussion, recipient)
  63. DiscussionReader.for(discussion: discussion, user: recipient).volume_is_loud?
  64. end
  65. def reply_to_address(model:, user: )
  66. letter = {
  67. 'Comment' => 'c',
  68. 'Poll' => 'p',
  69. 'Stance' => 's',
  70. 'Outcome' => 'o'
  71. }[model.class.to_s]
  72. address = {
  73. pt: letter,
  74. pi: letter ? model.id : nil,
  75. d: model.discussion_id,
  76. u: user.id,
  77. k: user.email_api_key
  78. }.compact.map { |k, v| [k,v].join('=') }.join('&')
  79. [address, ENV['REPLY_HOSTNAME']].join('@')
  80. end
  81. def mark_summary_as_read_url_for(user, format: nil)
  82. email_actions_mark_summary_email_as_read_url(unsubscribe_token: user.unsubscribe_token,
  83. time_start: @time_start.utc.to_i,
  84. time_finish: @time_finish.utc.to_i,
  85. format: format)
  86. end
  87. def option_name(name, format, zone, date_time_pref)
  88. case format
  89. when 'i18n'
  90. t(name)
  91. when 'iso8601'
  92. format_iso8601_for_humans(name, zone, date_time_pref)
  93. else
  94. name
  95. end
  96. end
  97. def google_pie_chart_url(poll)
  98. pie_chart_url(scores: proposal_sparkline(poll), colors: proposal_colors(poll))
  99. end
  100. def proposal_sparkline(poll)
  101. poll.results.map {|h| h[:score] }.join(',')
  102. end
  103. def proposal_colors(poll)
  104. poll.results.map{|h|h[:color]}.map{|c| c.gsub('#', '')}.join(',')
  105. end
  106. def dot_vote_stance_choice_percentage_for(stance, stance_choice)
  107. max = stance.poll.dots_per_person.to_i
  108. if max > 0
  109. (100 * stance_choice.score.to_f / max).to_i
  110. else
  111. 0
  112. end
  113. end
  114. def score_stance_choice_percentage_for(stance, stance_choice)
  115. max = stance.poll.max_score.to_i
  116. if max > 0
  117. (100 * stance_choice.score.to_f / max).to_i
  118. else
  119. 0
  120. end
  121. end
  122. def optional_link(url, attrs = {}, &block)
  123. if url
  124. content_tag(:a, {href: url}.merge(attrs)) do
  125. block.call
  126. end
  127. else
  128. content_tag(:span) do
  129. block.call
  130. end
  131. end
  132. end
  133. end

app/helpers/formatted_date_helper.rb

0.0% lines covered

50 relevant lines. 0 lines covered and 50 lines missed.
    
  1. module FormattedDateHelper
  2. def format_iso8601_for_humans(str, zone, date_time_pref)
  3. format_date_for_humans(parse_date_or_datetime(str), zone, date_time_pref)
  4. end
  5. def format_date_for_humans(date, zone, date_time_pref)
  6. format_date_or_datetime(date.in_time_zone(zone), date_time_pref)
  7. end
  8. def is_datetime?(value)
  9. value.is_a?(DateTime) or value.is_a?(Time) or value.is_a?(ActiveSupport::TimeWithZone)
  10. end
  11. def parse_date_or_datetime(value)
  12. return parse_datetime(value) if is_datetime_string?(value)
  13. if is_datetime?(value)
  14. value
  15. else
  16. value.to_date
  17. end
  18. end
  19. def is_datetime_string?(value)
  20. !!parse_datetime(value)
  21. rescue ArgumentError
  22. false
  23. end
  24. def parse_datetime(value)
  25. DateTime.strptime(value.sub('.000Z', 'Z'))
  26. end
  27. def format_date_or_datetime(value, date_time_pref)
  28. case date_time_pref
  29. when 'iso'
  30. date_format = '%Y-%m-%d'
  31. time_format = '%H:%M'
  32. when 'day_iso'
  33. date_format = '%a %Y-%m-%d'
  34. time_format = '%H:%M'
  35. when 'abbr'
  36. date_format = '%e %b %Y'
  37. time_format = '%l:%M%p'
  38. when 'day_abbr'
  39. date_format = '%a %e %b %Y'
  40. time_format = '%l:%M%p'
  41. else
  42. raise "unknown date pref"
  43. end
  44. if is_datetime?(value)
  45. value.strftime("#{date_format} #{time_format}")
  46. else
  47. value.strftime("#{date_format}")
  48. end
  49. end
  50. end

app/helpers/load_and_authorize.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module LoadAndAuthorize
  2. def load_and_authorize(model, action = :show, optional: false)
  3. return if optional && !(params[:"#{model}_id"] || params[:"#{model}_key"])
  4. instance_variable_set :"@#{model}", ModelLocator.new(model, params).locate!
  5. current_user.ability.authorize! action, instance_variable_get(:"@#{model}")
  6. end
  7. end

app/helpers/locales_helper.rb

0.0% lines covered

67 relevant lines. 0 lines covered and 67 lines missed.
    
  1. module LocalesHelper
  2. def process_time_zone(&block)
  3. Time.use_zone(TimeZoneToCity.convert(current_user.time_zone.to_s), &block)
  4. end
  5. def use_preferred_locale
  6. I18n.locale = preferred_locale
  7. yield if block_given?
  8. save_detected_locale
  9. end
  10. def preferred_locale
  11. # allow unsupported locales via params
  12. normalize(params[:locale]) ||
  13. first_supported_locale(user_selected_locale,
  14. browser_detected_locales,
  15. user_detected_locale)
  16. end
  17. def logged_out_preferred_locale
  18. normalize(params[:locale]) ||
  19. first_supported_locale(browser_detected_locales)
  20. end
  21. def supported_locales
  22. AppConfig.locales['supported']
  23. end
  24. def save_detected_locale(user = current_user)
  25. if user.is_logged_in? && browser_detected_locales.any?
  26. user.update_detected_locale(first_supported_locale browser_detected_locales)
  27. end
  28. end
  29. def first_supported_locale(*locales)
  30. Array(locales).flatten.compact.map do |locale|
  31. [normalize(locale),
  32. strip_dialect(locale),
  33. fallback_for(locale)].detect do |version|
  34. supported_locales.include? version
  35. end
  36. end.compact.first || I18n.default_locale
  37. end
  38. def help_manual_locale(locale)
  39. "en"
  40. end
  41. private
  42. def normalize(locale)
  43. return unless locale
  44. lang, dialect = locale.to_s.sub('-', '_').split('_')
  45. [lang&.downcase, dialect&.upcase].compact.join('_')
  46. end
  47. def strip_dialect(locale)
  48. locale.to_s.split('_').first
  49. end
  50. def fallback_for(locale)
  51. fallbacks[locale] || fallbacks[strip_dialect(locale)]
  52. end
  53. def fallbacks
  54. AppConfig.locales['fallbacks']
  55. end
  56. def user_selected_locale
  57. return nil unless current_user&.is_logged_in?
  58. current_user.selected_locale
  59. end
  60. def user_detected_locale
  61. return unless current_user&.is_logged_in?
  62. current_user.detected_locale
  63. end
  64. def browser_detected_locales
  65. parser = HttpAcceptLanguage::Parser.new(request.env["HTTP_ACCEPT_LANGUAGE"])
  66. parser.user_preferred_languages.map {|locale| normalize locale }
  67. end
  68. end

app/helpers/pending_actions_helper.rb

0.0% lines covered

93 relevant lines. 0 lines covered and 93 lines missed.
    
  1. module PendingActionsHelper
  2. private
  3. def handle_pending_actions(user = current_user)
  4. if user.is_logged_in?
  5. session.delete(:pending_user_id) if pending_user
  6. consume_pending_login_token
  7. consume_pending_identity(user)
  8. consume_pending_group(user)
  9. consume_pending_membership(user)
  10. consume_pending_discussion_reader(user)
  11. consume_pending_stance(user)
  12. session.delete(:pending_login_token)
  13. session.delete(:pending_identity_id)
  14. session.delete(:pending_group_token)
  15. session.delete(:pending_discussion_reader_token)
  16. session.delete(:pending_stance_token)
  17. end
  18. end
  19. def consume_pending_login_token
  20. pending_login_token.update(used: true) if pending_login_token
  21. end
  22. def consume_pending_identity(user)
  23. user.associate_with_identity(pending_identity) if pending_identity
  24. end
  25. def consume_pending_group(user)
  26. RetryOnError.with_limit(2) do
  27. if pending_group && !Membership.where(user_id: user.id, group_id: pending_group.id).exists?
  28. membership = pending_group.memberships.create(user: user)
  29. MembershipService.redeem(membership: membership, actor: user)
  30. end
  31. end
  32. end
  33. def consume_pending_membership(user)
  34. if pending_membership
  35. MembershipService.redeem(membership: pending_membership, actor: user)
  36. end
  37. end
  38. # memberships are valid even if not accepted, but this lets us know if people are using them
  39. def accept_pending_membership
  40. group = @group or (@discussion && @discussion.group) or (@poll && @poll.group)
  41. return unless group
  42. MembershipService.redeem_if_pending!(group.membership_for(current_user))
  43. end
  44. def consume_pending_discussion_reader(user)
  45. if reader = pending_discussion_reader
  46. DiscussionReaderService.redeem(discussion_reader: reader, actor: user)
  47. end
  48. end
  49. def consume_pending_stance(user)
  50. StanceService.redeem(stance: pending_stance, actor: user) if pending_stance
  51. end
  52. def pending_group
  53. Group.find_by(token: session[:pending_group_token]) if session[:pending_group_token]
  54. end
  55. def pending_login_token
  56. LoginToken.find_by(token: session[:pending_login_token]) if session[:pending_login_token]
  57. end
  58. def pending_invitation
  59. pending_membership || pending_discussion_reader || pending_stance
  60. end
  61. def pending_membership_token
  62. params[:membership_token] || session[:pending_membership_token]
  63. end
  64. def pending_membership
  65. Membership.pending.find_by(token: pending_membership_token) if pending_membership_token
  66. end
  67. def pending_discussion_reader_token
  68. params[:discussion_reader_token] || session[:pending_discussion_reader_token]
  69. end
  70. def pending_discussion_reader
  71. DiscussionReader.redeemable.find_by(token: pending_discussion_reader_token) if pending_discussion_reader_token
  72. end
  73. def pending_stance_token
  74. params[:stance_token] || session[:pending_stance_token]
  75. end
  76. def pending_stance
  77. Stance.find_by(token: pending_stance_token) if pending_stance_token
  78. end
  79. def pending_identity
  80. Identities::Base.find_by(id: session[:pending_identity_id]) if session[:pending_identity_id]
  81. end
  82. def pending_user
  83. User.find_by(id: session[:pending_user_id]) if session[:pending_user_id]
  84. end
  85. def serialized_pending_identity
  86. Pending::TokenSerializer.new(pending_login_token, root: false).as_json ||
  87. Pending::IdentitySerializer.new(pending_identity, root: false).as_json ||
  88. Pending::MembershipSerializer.new(pending_membership, root: false).as_json ||
  89. Pending::StanceSerializer.new(pending_stance, root: false).as_json ||
  90. Pending::DiscussionReaderSerializer.new(pending_discussion_reader, root: false).as_json ||
  91. Pending::GroupSerializer.new(pending_group, root: false).as_json ||
  92. Pending::UserSerializer.new(pending_user, root: false).as_json || {}
  93. end
  94. end

app/helpers/pretty_url_helper.rb

0.0% lines covered

59 relevant lines. 0 lines covered and 59 lines missed.
    
  1. module PrettyUrlHelper
  2. include Routing
  3. def join_url(model, opts = {})
  4. super opts.merge(model: model.class.to_s.underscore, token: model.group.token)
  5. end
  6. def discussion_path(discussion, options = {})
  7. super(discussion, options.merge(slug: discussion.title.parameterize))
  8. end
  9. def discussion_url(discussion, options = {})
  10. super(discussion, options.merge(slug: discussion.title.parameterize))
  11. end
  12. def group_url(group, options = {})
  13. if group.handle and !options.delete(:use_key)
  14. group_handle_url(group.handle, options)
  15. else
  16. super group, options.merge(slug: group.name.parameterize)
  17. end
  18. end
  19. def discussion_poll_url(model, options = {})
  20. if model.discussion.present?
  21. if model.is_a?(Outcome)
  22. discussion_url(model.discussion, options.merge(sequence_id: model.poll.created_event.sequence_id))
  23. else
  24. discussion_url(model.discussion, options.merge(sequence_id: model.created_event.sequence_id))
  25. end
  26. else
  27. poll_url(model.poll, options)
  28. end
  29. end
  30. def polymorphic_url(model, opts = {})
  31. case model
  32. when NilClass, LoggedOutUser then nil
  33. when Group, GroupIdentity then group_url(model.group, opts)
  34. when PaperTrail::Version then polymorphic_url(model.item, opts)
  35. when MembershipRequest then group_url(model.group, opts.merge(use_key: true))
  36. when Poll then discussion_poll_url(model, opts)
  37. when Outcome then discussion_poll_url(model, opts)
  38. when Stance then discussion_poll_url(model, opts)
  39. when Comment then comment_url(model.discussion, model, opts)
  40. when Membership then membership_url(model, opts)
  41. when Reaction then polymorphic_url(model.reactable, opts)
  42. when ReceivedEmail then group_emails_url(model.group.key)
  43. else super
  44. end
  45. end
  46. def polymorphic_path(model, opts = {})
  47. # angular router throws error if you give it a whole url
  48. polymorphic_url(model, opts).sub(root_url, '')
  49. end
  50. def polymorphic_title(model)
  51. case model
  52. when PaperTrail::Version then model.item.title
  53. when Comment, Discussion then model.discussion.title
  54. when Poll, Outcome, Stance then model.poll.title
  55. when Reaction then model.reactable.title
  56. when Group then model.full_name
  57. when Membership then polymorphic_title(model.group)
  58. end
  59. end
  60. end

app/helpers/protected_from_forgery.rb

0.0% lines covered

19 relevant lines. 0 lines covered and 19 lines missed.
    
  1. module ProtectedFromForgery
  2. def self.included(base)
  3. base.after_action :set_xsrf_token
  4. end
  5. protected
  6. def verified_request?
  7. super || Rails.env.development? || cookies['csrftoken'] == request.headers['X-CSRF-TOKEN']
  8. end
  9. private
  10. def set_xsrf_token
  11. if protect_against_forgery?
  12. cookies[:csrftoken] = {
  13. value: form_authenticity_token,
  14. expires: 1.day.from_now,
  15. secure: true
  16. }
  17. end
  18. end
  19. end

app/helpers/sentry_helper.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module SentryHelper
  2. def set_sentry_context
  3. Sentry.configure_scope do |scope|
  4. scope.set_user(id: current_user.id, ip_address: request.remote_ip)
  5. scope.set_tags(email: current_user.email, name: current_user.name)
  6. # scope.set_extra(params: params.to_unsafe_h, url: request.url)
  7. end
  8. end
  9. end

app/helpers/uses_metadata.rb

62.5% lines covered

16 relevant lines. 10 lines covered and 6 lines missed.
    
  1. 1 module UsesMetadata
  2. 1 def show
  3. 1 metadata
  4. respond_to do |format|
  5. format.html { index }
  6. format.rss { render :"show.xml" }
  7. format.xml
  8. end
  9. end
  10. 1 private
  11. 1 def metadata
  12. 1 @metadata ||= if current_user.can? :show, resource
  13. "Metadata::#{controller_name.singularize.camelize}Serializer".constantize.new(resource)
  14. else
  15. {}
  16. end.as_json
  17. end
  18. 1 def resource
  19. 1 instance_variable_get("@#{resource_name}") ||
  20. instance_variable_set("@#{resource_name}", ModelLocator.new(resource_name, params).locate)
  21. end
  22. 1 def resource_name
  23. 3 controller_name.singularize
  24. end
  25. end

app/mailers/base_mailer.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 1 class BaseMailer < ActionMailer::Base
  2. 1 include ERB::Util
  3. 1 include ActionView::Helpers::TextHelper
  4. 1 include EmailHelper
  5. 1 include LocalesHelper
  6. 1 helper :email
  7. 1 helper :formatted_date
  8. 1 NOTIFICATIONS_EMAIL_ADDRESS = ENV.fetch('NOTIFICATIONS_EMAIL_ADDRESS', "notifications@#{ENV['SMTP_DOMAIN']}")
  9. 1 default :from => "\"#{AppConfig.theme[:site_name]}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
  10. 1 before_action :utm_hash
  11. 1 protected
  12. 1 def utm_hash
  13. 1022 @utm_hash = { utm_medium: 'email', utm_campaign: action_name }
  14. end
  15. 1 def from_user_via_loomio(user)
  16. 992 if user.present?
  17. 983 "\"#{I18n.t('base_mailer.via_loomio', name: user.name, site_name: AppConfig.theme[:site_name])}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
  18. else
  19. 9 "\"#{AppConfig.theme[:site_name]}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
  20. end
  21. end
  22. 1 def send_single_mail(locale: , to:, subject_key:, subject_params: {}, subject_prefix: '', subject_is_title: false, **options)
  23. 1021 return if NoSpam::SPAM_REGEX.match?(to)
  24. 1021 return if NOTIFICATIONS_EMAIL_ADDRESS == to
  25. 1021 I18n.with_locale(first_supported_locale(locale)) do
  26. 1021 if subject_is_title
  27. 28 subject = subject_prefix + subject_params[:title]
  28. else
  29. 993 subject = subject_prefix + I18n.t(subject_key, **subject_params)
  30. end
  31. 1021 mail options.merge(to: to, subject: subject )
  32. end
  33. rescue Net::SMTPSyntaxError, Net::SMTPFatalError => e
  34. raise "SMTP error to: '#{to}' from: '#{options[:from]}' action: #{action_name} mailer: #{mailer_name} error: #{e}"
  35. end
  36. end

app/mailers/contact_mailer.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class ContactMailer < ActionMailer::Base
  2. default :from => "\"#{AppConfig.theme[:site_name]}\" <#{BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS}>"
  3. def contact_message(name, email, subject, body, details = {})
  4. @details = details
  5. @body = body
  6. mail(
  7. to: ENV['SUPPORT_EMAIL'],
  8. reply_to: "\"#{name}\" <#{email}>",
  9. subject: subject,
  10. )
  11. end
  12. end

app/mailers/event_mailer.rb

97.62% lines covered

42 relevant lines. 41 lines covered and 1 lines missed.
    
  1. 1 class EventMailer < BaseMailer
  2. 1 REPLY_DELIMITER = ""*4 # surprise! this is actually U+FEFF
  3. # TODO this should be NotificationMailer, and take a notification id
  4. 1 def event(recipient_id, event_id)
  5. 991 @current_user = @recipient = User.active.find_by!(id: recipient_id)
  6. 991 @event = Event.find_by!(id: event_id)
  7. 991 @notification = Notification.find_by(user_id: recipient_id, event_id: event_id)
  8. 991 return if @event.eventable.nil?
  9. 991 return if @event.eventable.respond_to?(:discarded?) && @event.eventable.discarded?
  10. 991 if %w[Poll Stance Outcome].include? @event.eventable_type
  11. 930 @poll = @event.eventable.poll
  12. end
  13. 991 if @event.eventable.respond_to? :discussion
  14. 976 @discussion = @event.eventable.discussion
  15. end
  16. 991 if @event.eventable.respond_to?(:group_id) && @event.eventable.group_id
  17. 991 @membership = Membership.active.find_by(
  18. group_id: @event.eventable.group_id,
  19. user_id: recipient_id
  20. )
  21. # this might be necessary to comply with anti-spam rules
  22. # if someone does not respond to the invitation, don't send them more emails
  23. 991 return if @membership &&
  24. !@recipient.email_verified &&
  25. !["membership_created", "membership_resent"].include?(@event.kind)
  26. end
  27. 991 @utm_hash = { utm_medium: 'email', utm_campaign: @event.kind }
  28. thread_kinds = %w[
  29. 991 new_comment
  30. new_discussion
  31. discussion_edited
  32. discussion_announced
  33. ]
  34. 991 headers = {
  35. "Precedence": :bulk,
  36. "X-Auto-Response-Suppress": :OOF,
  37. "Auto-Submitted": :"auto-generated"
  38. }
  39. 991 if @event.eventable.respond_to?(:calendar_invite) && @event.eventable.calendar_invite
  40. 2 attachments['meeting.ics'] = {
  41. content_type: 'text/calendar',
  42. content_transfer_encoding: 'base64',
  43. content: Base64.encode64(@event.eventable.calendar_invite)
  44. }
  45. end
  46. 991 template_name = @event.eventable_type.tableize.singularize
  47. 991 template_name = 'poll' if @event.eventable_type == 'Outcome'
  48. 991 template_name = 'group' if @event.eventable_type == 'Membership'
  49. # this should be notification.i18n_key
  50. 991 @event_key = if (@event.kind == 'user_mentioned' &&
  51. @event.eventable.respond_to?(:parent) &&
  52. @event.eventable.parent.present? &&
  53. @event.eventable.parent.author == @recipient)
  54. 3 "comment_replied_to"
  55. 988 elsif @event.kind == 'poll_created'
  56. 504 'poll_announced'
  57. else
  58. 484 @event.kind
  59. end
  60. subject_params = {
  61. 991 title: @event.eventable.title,
  62. group_name: @event.eventable.title, # cope for old translations
  63. poll_type: @poll && I18n.t("poll_types.#{@poll.poll_type}"),
  64. actor: @event.user.name,
  65. site_name: AppConfig.theme[:site_name]
  66. }
  67. 991 send_single_mail(
  68. to: @recipient.email,
  69. from: from_user_via_loomio(@event.user),
  70. locale: @recipient.locale,
  71. reply_to: reply_to_address_with_group_name(model: @event.eventable, user: @recipient),
  72. subject_prefix: group_name_prefix(@event),
  73. subject_key: "notifications.with_title.#{@event_key}",
  74. subject_params: subject_params,
  75. subject_is_title: thread_kinds.include?(@event.kind),
  76. template_name: template_name
  77. )
  78. end
  79. 1 private
  80. 1 def group_name_prefix(event)
  81. 991 model = event.eventable
  82. 991 if %w[membership_requested membership_created].include? event.kind
  83. 11 ''
  84. else
  85. 980 model.group.present? ? "[#{model.group.handle || model.group.full_name}] " : ''
  86. end
  87. end
  88. 1 def reply_to_address_with_group_name(model:, user:)
  89. 991 return nil unless user.is_logged_in?
  90. 991 return nil unless model.respond_to?(:discussion) && model.discussion.present?
  91. 953 if model.discussion.group.present?
  92. 953 "\"#{I18n.transliterate(model.discussion.group.full_name).truncate(50).delete('"')}\" <#{reply_to_address(model: model, user: user)}>"
  93. else
  94. "\"#{user.name}\" <#{reply_to_address(model: model, user: user)}>"
  95. end
  96. end
  97. end

app/mailers/forward_mailer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class ForwardMailer < ActionMailer::Base
  2. 1 layout nil
  3. 1 def forward_message(to:, from:, reply_to:, subject:, body_text: nil, body_html: nil)
  4. 1 @body_text = body_text
  5. 1 @body_html = body_html
  6. 1 mail(
  7. from: from,
  8. to: to,
  9. reply_to: reply_to,
  10. subject: subject,
  11. layout: nil
  12. )
  13. end
  14. end

app/mailers/group_mailer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class GroupMailer < BaseMailer
  2. 1 def destroy_warning(group_id, recipient_id, deletor_id)
  3. 2 @group = Group.find(group_id)
  4. 2 @recipient = User.find(recipient_id)
  5. 2 @deletor = User.find(deletor_id)
  6. 2 send_single_mail to: @recipient.name_and_email,
  7. reply_to: ENV['SUPPORT_EMAIL'],
  8. subject_key: "group_mailer.destroy_warning.subject",
  9. locale: @recipient.locale
  10. end
  11. end

app/mailers/task_mailer.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class TaskMailer < BaseMailer
  2. 1 def task_due_reminder(recipient, task)
  3. 1 @recipient = recipient
  4. 1 @task = task
  5. 1 send_single_mail(
  6. locale: @recipient.locale,
  7. to: @recipient.name_and_email,
  8. subject_key: 'task_mailer.task_due_reminder.subject',
  9. subject_params: {name: @task.name},
  10. )
  11. end
  12. end

app/mailers/user_mailer.rb

86.54% lines covered

52 relevant lines. 45 lines covered and 7 lines missed.
    
  1. 1 class UserMailer < BaseMailer
  2. 1 def redacted(email, locale)
  3. 4 send_single_mail to: email,
  4. subject_key: "user_mailer.redacted.subject",
  5. subject_params: { site_name: AppConfig.theme[:site_name] },
  6. locale: locale
  7. end
  8. 1 def accounts_merged(user_id)
  9. 2 @user = User.find(user_id)
  10. 2 @token = @user.login_tokens.create!
  11. 2 send_single_mail to: @user.email,
  12. subject_key: "user_mailer.accounts_merged.subject",
  13. subject_params: { site_name: AppConfig.theme[:site_name] },
  14. locale: @user.locale
  15. end
  16. 1 def merge_verification(source_user:, target_user:, hash:)
  17. @source_user = source_user
  18. @target_user = target_user
  19. @hash = hash
  20. send_single_mail to: @target_user.email,
  21. subject_key: "user_mailer.merge_verification.subject",
  22. subject_params: {site_name: AppConfig.theme[:site_name]},
  23. locale: @target_user.locale
  24. end
  25. 1 def catch_up(user_id, time_since = nil, frequency = 'daily')
  26. 4 user = User.find(user_id)
  27. 4 return unless user.email_catch_up_day
  28. 4 @current_user = @recipient = @user = user
  29. 4 if frequency == 'daily'
  30. 3 @time_start = time_since || 24.hours.ago
  31. 1 elsif frequency == 'other'
  32. @time_start = time_since || 48.hours.ago
  33. else
  34. 1 @time_start = time_since || 1.week.ago
  35. end
  36. 4 @time_finish = Time.zone.now
  37. 4 @time_frame = @time_start...@time_finish
  38. 4 @discussions = DiscussionQuery.visible_to(
  39. user: user,
  40. only_unread: true,
  41. or_public: false,
  42. or_subgroups: false).last_activity_after(@time_start)
  43. 4 @groups = @user.groups.order(full_name: :asc)
  44. 4 @cache = RecordCache.for_collection(@discussions, user_id)
  45. 4 @subject_key = "email.catch_up.#{frequency}_subject"
  46. 4 @subject_params = { site_name: AppConfig.theme[:site_name] }
  47. 4 unless @discussions.empty?
  48. 3 @discussions_by_group_id = @discussions.group_by(&:group_id)
  49. 3 send_single_mail to: @user.email,
  50. subject_key: @subject_key,
  51. subject_params: @subject_params,
  52. locale: @user.locale
  53. end
  54. end
  55. 1 def membership_request_approved(recipient_id, event_id)
  56. 5 @user = User.find_by(id: recipient_id)
  57. 5 @group = Event.find_by(id: event_id).eventable.group
  58. 5 send_single_mail to: @user.email,
  59. reply_to: @group.admin_email,
  60. subject_key: "email.group_membership_approved.subject",
  61. subject_params: {group_name: @group.full_name},
  62. locale: @user.locale
  63. end
  64. 1 def user_added_to_group(recipient_id, event_id)
  65. 1 @user = User.find_by!(id: recipient_id)
  66. 1 event = Event.find_by!(id: event_id)
  67. 1 @group = event.eventable.group
  68. 1 @inviter = event.eventable.inviter || @group.admins.first
  69. 1 send_single_mail to: @user.email,
  70. from: from_user_via_loomio(@inviter),
  71. reply_to: @inviter.try(:name_and_email),
  72. subject_key: "email.user_added_to_group.subject",
  73. subject_params: { which_group: @group.full_name, who: @inviter.name, site_name: AppConfig.theme[:site_name] },
  74. locale: [@user.locale, @inviter.locale]
  75. end
  76. 1 def group_export_ready(recipient_id, group_name, document_id)
  77. 1 @user = User.find(recipient_id)
  78. 1 @document = Document.find(document_id)
  79. 1 send_single_mail to: @user.email,
  80. subject_key: "user_mailer.group_export_ready.subject",
  81. subject_params: {group_name: group_name},
  82. locale: @user.locale
  83. end
  84. 1 def login(user_id, token_id)
  85. 11 @user = User.find_by!(id: user_id)
  86. 11 @token = LoginToken.find_by!(id: token_id)
  87. 11 send_single_mail to: @user.email,
  88. subject_key: "email.login.subject",
  89. subject_params: {site_name: AppConfig.theme[:site_name]},
  90. locale: @user.locale
  91. end
  92. 1 def contact_request(contact_request:)
  93. @contact_request = contact_request
  94. send_single_mail to: @contact_request.recipient.email,
  95. from: from_user_via_loomio(@contact_request.sender),
  96. reply_to: @contact_request.sender.name_and_email,
  97. subject_key: "email.contact_request.subject",
  98. subject_params: { name: @contact_request.sender.name,
  99. site_name: AppConfig.theme[:site_name]},
  100. locale: [@contact_request.recipient.locale, @contact_request.sender.locale]
  101. end
  102. end

app/models/ability/attachment.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. 1 module Ability::Attachment
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::Attachment do |attachment|
  5. user.groups.exists?(attachment.record.group.id)
  6. end
  7. 1713 can :destroy, ::Attachment do |attachment|
  8. 2 user.adminable_groups.exists?(attachment.record.group.id)
  9. end
  10. end
  11. end

app/models/ability/base.rb

94.59% lines covered

37 relevant lines. 35 lines covered and 2 lines missed.
    
  1. 1 module Ability
  2. 1 class Base
  3. 1 include CanCan::Ability
  4. 1 prepend Ability::Comment
  5. 1 prepend Ability::DiscussionReader
  6. 1 prepend Ability::Discussion
  7. 1 prepend Ability::Document
  8. 1 prepend Ability::Group
  9. 1 prepend Ability::Identity
  10. 1 prepend Ability::MembershipRequest
  11. 1 prepend Ability::Membership
  12. 1 prepend Ability::Outcome
  13. 1 prepend Ability::Poll
  14. 1 prepend Ability::Reaction
  15. 1 prepend Ability::Stance
  16. 1 prepend Ability::User
  17. 1 prepend Ability::Tag
  18. 1 prepend Ability::Event
  19. 1 prepend Ability::Chatbot
  20. 1 prepend Ability::Attachment
  21. 1 prepend Ability::Task
  22. 1 prepend Ability::PollTemplate
  23. 1 prepend Ability::DiscussionTemplate
  24. 1 def initialize(user)
  25. 1713 @user = user
  26. 1713 can(:subscribe_to, GlobalMessageChannel) { true }
  27. end
  28. 1 private
  29. 1 def user_is_member_of?(group_id)
  30. 21 @user.memberships.find_by(group_id: group_id)
  31. end
  32. 1 def user_is_admin_of?(group_id)
  33. 32 @user.admin_memberships.find_by(group_id: group_id)
  34. end
  35. 1 def user_is_member_of_any?(groups)
  36. @user.memberships.find_by(group: groups)
  37. end
  38. 1 def user_is_admin_of_any?(groups)
  39. @user.admin_memberships.find_by(group: groups)
  40. end
  41. 1 def user_is_author_of?(object)
  42. 2 @user.is_logged_in? && @user.id == object.author_id
  43. end
  44. end
  45. end

app/models/ability/chatbot.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 module Ability::Chatbot
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:create, :destroy, :update, :test], ::Chatbot do |chatbot|
  5. chatbot.group.admins.exists?(user.id)
  6. end
  7. end
  8. end

app/models/ability/comment.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 module Ability::Comment
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:create], ::Comment do |comment|
  5. 191 comment.discussion &&
  6. !comment.discussion.closed_at &&
  7. comment.discussion.members.exists?(user.id)
  8. end
  9. 1713 can [:update], ::Comment do |comment|
  10. 13 !comment.discussion.closed_at && (
  11. 13 (comment.discussion.members.exists?(user.id) && comment.author == user && comment.group.members_can_edit_comments) ||
  12. 6 (comment.discussion.admins.exists?(user.id) && comment.group.admins_can_edit_user_content)
  13. )
  14. end
  15. 1713 can [:discard, :undiscard], ::Comment do |comment|
  16. 3 !comment.discussion.closed_at &&
  17. (
  18. 3 (comment.author == user && comment.discussion.members.exists?(user.id)) ||
  19. comment.discussion.admins.exists?(user.id)
  20. )
  21. end
  22. 1713 can [:destroy], ::Comment do |comment|
  23. 9 !comment.discussion.closed_at &&
  24. Comment.where(parent: comment).count == 0 &&
  25. (
  26. 8 comment.discussion.admins.exists?(user.id) ||
  27. 4 (comment.author == user &&
  28. comment.discussion.members.exists?(user.id) &&
  29. comment.group.members_can_delete_comments)
  30. )
  31. end
  32. 1713 can [:show], ::Comment do |comment|
  33. 9 can?(:show, comment.discussion) && comment.kept?
  34. end
  35. end
  36. end

app/models/ability/discussion.rb

91.67% lines covered

36 relevant lines. 33 lines covered and 3 lines missed.
    
  1. 1 module Ability::Discussion
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:show,
  5. :print,
  6. :dismiss,
  7. :subscribe_to], ::Discussion do |discussion|
  8. 114 DiscussionQuery.visible_to(user: user).exists?(discussion.id)
  9. end
  10. 1713 can [:mark_as_read, :mark_as_seen], ::Discussion do |discussion|
  11. 5 user.is_logged_in? && can?(:show, discussion)
  12. end
  13. 1713 can :update_version, ::Discussion do |discussion|
  14. discussion.author == user or discussion.admins.exists?(user.id)
  15. end
  16. 1713 can :create, ::Discussion do |discussion|
  17. 375 user.email_verified? &&
  18. (
  19. 375 discussion.group.blank? ||
  20. discussion.group.admins.exists?(user.id) ||
  21. 266 (discussion.group.members_can_start_discussions && discussion.group.members.exists?(user.id))
  22. )
  23. end
  24. 1713 can [:announce], ::Discussion do |discussion|
  25. 13 if discussion.group_id
  26. 10 discussion.group.admins.exists?(user.id) ||
  27. 8 (discussion.group.members_can_announce && discussion.members.exists?(user.id))
  28. else
  29. 3 discussion.admins.exists?(user.id)
  30. end
  31. end
  32. 1713 can [:add_members], ::Discussion do |discussion|
  33. 814 discussion.members.exists?(user.id)
  34. end
  35. 1713 can [:add_guests], ::Discussion do |discussion|
  36. 824 if discussion.group_id
  37. 796 Subscription.for(discussion.group).allow_guests &&
  38. 796 (discussion.group.admins.exists?(user.id) || (discussion.group.members_can_add_guests && discussion.members.exists?(user.id)))
  39. else
  40. 28 !discussion.id || discussion.admins.exists?(user.id)
  41. end
  42. end
  43. 1713 can [:update, :move, :move_comments, :pin], ::Discussion do |discussion|
  44. 63 discussion.discarded_at.nil? &&
  45. 63 (discussion.author == user ||
  46. discussion.admins.exists?(user.id) ||
  47. 23 (discussion.group.members_can_edit_discussions && discussion.members.exists?(user.id)))
  48. end
  49. 1713 can [:destroy, :discard], ::Discussion do |discussion|
  50. 5 discussion.discarded_at.nil? &&
  51. 5 (discussion.author == user || discussion.admins.exists?(user.id))
  52. end
  53. 1713 can [:set_volume], ::Discussion do |discussion|
  54. discussion.members.exists?(user.id)
  55. end
  56. 1713 can :remove_events, ::Discussion do |discussion|
  57. discussion.author == user or discussion.admins.exists?(user.id)
  58. end
  59. end
  60. end

app/models/ability/discussion_reader.rb

63.64% lines covered

11 relevant lines. 7 lines covered and 4 lines missed.
    
  1. 1 module Ability::DiscussionReader
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:update], ::DiscussionReader do |discussion_reader|
  5. discussion_reader.user.id == user.id
  6. end
  7. 1713 can [:redeem], ::DiscussionReader do |discussion_reader|
  8. DiscussionReader.redeemable.exists?(discussion_reader.id)
  9. end
  10. 1713 can [:make_admin, :remove_admin, :resend], ::DiscussionReader do |discussion_reader|
  11. discussion_reader.discussion.admins.exists?(user.id)
  12. end
  13. 1713 can [:remove], ::DiscussionReader do |discussion_reader|
  14. discussion_reader.guest && discussion_reader.discussion.admins.exists?(user.id)
  15. end
  16. end
  17. end

app/models/ability/discussion_template.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 module Ability::DiscussionTemplate
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can([:create, :update], ::DiscussionTemplate) do |discussion_template|
  5. discussion_template.group.admins.exists?(user.id)
  6. end
  7. end
  8. end

app/models/ability/document.rb

55.56% lines covered

9 relevant lines. 5 lines covered and 4 lines missed.
    
  1. 1 module Ability::Document
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:create, :update], ::Document do |document|
  5. if document.model.presence
  6. user.can? :update, document.model
  7. else
  8. user.email_verified?
  9. end
  10. end
  11. 1713 can :destroy, ::Document do |document|
  12. user_is_admin_of? document.model.group.id
  13. end
  14. end
  15. end

app/models/ability/event.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module Ability
  2. 1 module Event
  3. 1 def initialize(user)
  4. 1713 super(user)
  5. 1713 can [:pin, :unpin], ::Event do |event|
  6. 1 event.discussion && can?(:update, event.discussion)
  7. end
  8. end
  9. end
  10. end

app/models/ability/group.rb

95.45% lines covered

44 relevant lines. 42 lines covered and 2 lines missed.
    
  1. 1 module Ability::Group
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:show], ::Group do |group|
  5. 106 !group.archived_at &&
  6. (
  7. 106 group.is_visible_to_public? or
  8. group.members.exists?(user.id) or
  9. 12 (group.is_visible_to_parent_members? and user_is_member_of?(group.parent_id)) or
  10. 10 (user.group_token && user.group_token == group.token) or
  11. 9 (user.membership_token && group.memberships.pending.find_by(token: user.membership_token))
  12. )
  13. end
  14. 1713 can [:see_private_content, :subscribe_to], ::Group do |group|
  15. 10 !group.archived_at && (
  16. 10 group.group_privacy == 'open' or
  17. group.members.exists?(user.id) or
  18. 2 (group.is_visible_to_parent_members? and group.parent_or_self.members.exists?(user.id)))
  19. end
  20. 1713 can [:update,
  21. :email_members,
  22. :archive,
  23. :destroy,
  24. :publish,
  25. :export,
  26. :view_pending_invitations], ::Group do |group|
  27. 20 group.admins.exists?(user.id)
  28. end
  29. 1713 can [:members_autocomplete,
  30. :show_chatbots,
  31. :set_volume], ::Group do |group|
  32. user.email_verified? && group.members.exists?(user.id)
  33. end
  34. 1713 can [:move_discussions_to], ::Group do |group|
  35. 9 user.email_verified? &&
  36. 9 (group.admins.exists?(user.id) ||
  37. 9 (group.members_can_start_discussions? && group.members.exists?(user.id)))
  38. end
  39. 1713 can [:add_guests], ::Group do |group|
  40. 15 user.email_verified? && Subscription.for(group).is_active? &&
  41. 15 ((group.members_can_add_guests && group.members.exists?(user.id)) || group.admins.exists?(user.id))
  42. end
  43. 1713 can [:add_members,
  44. :invite_people,
  45. :announce,
  46. :manage_membership_requests], ::Group do |group|
  47. 55 user.is_admin ||
  48. (
  49. 51 ((group.members_can_add_members? && group.members.exists?(user.id)) ||
  50. group.admins.exists?(user.id))
  51. )
  52. end
  53. 1713 can [:notify], ::Group do |group|
  54. (group.members_can_announce && group.members.exists?(user.id)) || group.admins.exists?(user.id)
  55. end
  56. # please note that I don't like this duplication either.
  57. # add_subgroup checks against a parent group
  58. 1713 can [:add_subgroup], ::Group do |group|
  59. 2 user.email_verified? &&
  60. 2 (group.is_parent? &&
  61. group.members.exists?(user.id) &&
  62. 2 (group.members_can_create_subgroups? || group.admins.exists?(user.id)))
  63. end
  64. 1713 can :move, ::Group do |group|
  65. 2 user.is_admin
  66. end
  67. # create group checks against the group to be created
  68. 1713 can :create, ::Group do |group|
  69. # anyone can create a top level group of their own
  70. # otherwise, the group must be a subgroup
  71. # inwhich case we need to confirm membership and permission
  72. 11 (user.is_admin or AppConfig.app_features[:create_group]) &&
  73. user.email_verified? &&
  74. group.is_parent? ||
  75. 3 ( user_is_admin_of?(group.parent_id) ||
  76. 3 (user_is_member_of?(group.parent_id) && group.parent.members_can_create_subgroups?) )
  77. end
  78. 1713 can :join, ::Group do |group|
  79. 1 (user.email_verified? && can?(:show, group) && group.membership_granted_upon_request?) ||
  80. 1 (user_is_admin_of?(group.parent_id) && can?(:show, group) && group.membership_granted_upon_approval?)
  81. end
  82. 1713 can :merge, ::Group do |group|
  83. 3 user.is_admin?
  84. end
  85. end
  86. end

app/models/ability/identity.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 module Ability::Identity
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:show, :destroy], ::Identities::Base do |identity|
  5. user.identities.exists? identity.id
  6. end
  7. end
  8. end

app/models/ability/membership.rb

94.12% lines covered

17 relevant lines. 16 lines covered and 1 lines missed.
    
  1. 1 module Ability::Membership
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::Membership do |membership|
  5. membership.user_id == user.id || membership.group.admins.exists?(user.id) || membership.inviter_id == user.id
  6. end
  7. 1713 can [:update], ::Membership do |membership|
  8. 6 membership.user_id == user.id || membership.group.admins.exists?(user.id)
  9. end
  10. 1713 can [:make_admin], ::Membership do |membership|
  11. 6 membership.group.admins.exists?(user.id) ||
  12. 5 (user_is_member_of?(membership.group_id) && membership.user == user && membership.group.admin_memberships.count == 0) ||
  13. 4 (user_is_admin_of?(membership.group.parent_id) && user == membership.user)
  14. end
  15. 1713 can :resend, ::Membership do |membership|
  16. 3 !membership.accepted_at? && user_is_admin_of?(membership.group_id)
  17. end
  18. 1713 can [:remove_admin, :revoke, :destroy], ::Membership do |membership|
  19. 14 user.is_admin || (
  20. 13 (membership.user == user ||
  21. user_is_admin_of?(membership.group_id) ||
  22. 5 (membership.inviter == user && !membership.accepted_at?))
  23. )
  24. end
  25. end
  26. end

app/models/ability/membership_request.rb

81.82% lines covered

11 relevant lines. 9 lines covered and 2 lines missed.
    
  1. 1 module Ability::MembershipRequest
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :create, ::MembershipRequest do |request|
  5. group = request.group
  6. can?(:show, group) and group.membership_granted_upon_approval?
  7. end
  8. 1713 can :cancel, ::MembershipRequest, requestor_id: user.id
  9. 1713 can [:show, :approve, :ignore], ::MembershipRequest do |membership_request|
  10. 12 group = membership_request.group
  11. 12 user_is_admin_of?(group.id) or
  12. 10 (user_is_member_of?(group.id) and group.members_can_add_members?)
  13. end
  14. end
  15. end

app/models/ability/outcome.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module Ability::Outcome
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::Outcome do |outcome|
  5. 146 can? :show, outcome.poll
  6. end
  7. 1713 can [:create, :update], ::Outcome do |outcome|
  8. 34 !outcome.poll.active? &&
  9. 31 (outcome.admins.exists?(user.id) || (outcome.group.members_can_edit_discussions && outcome.members.exists?(user.id)))
  10. end
  11. 1713 can [:announce], ::Outcome do |outcome|
  12. 22 !outcome.poll.active? && can?(:announce, outcome.poll)
  13. end
  14. 1713 can [:add_members], ::Outcome do |outcome|
  15. 96 !outcome.poll.active? && can?(:add_members, outcome.poll)
  16. end
  17. 1713 can [:add_guests], ::Outcome do |outcome|
  18. 84 !outcome.poll.active? && can?(:add_guests, outcome.poll)
  19. end
  20. end
  21. end

app/models/ability/poll.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. 1 module Ability::Poll
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :vote_in, ::Poll do |poll|
  5. 91 user.is_logged_in? &&
  6. poll.active? &&
  7. (
  8. 88 poll.unmasked_voters.exists?(user.id) ||
  9. 21 (!poll.specified_voters_only && poll.members.exists?(user.id))
  10. )
  11. end
  12. 1713 can [:export], ::Poll do |poll|
  13. 3 user.can?(:show, poll) && poll.show_results?
  14. end
  15. 1713 can [:show], ::Poll do |poll|
  16. 268 PollQuery.visible_to(user: user, show_public: true).exists?(poll.id)
  17. end
  18. 1713 can [:create], ::Poll do |poll|
  19. 174 (poll.poll_template_id.nil? || poll.poll_template.public? || user.group_ids.include?(poll.poll_template.group_id)) &&
  20. 174 (poll.discussion_id.nil? || !poll.discussion.closed_at) &&
  21. (
  22. 174 (poll.group_id &&
  23. (
  24. 170 (poll.group.admins.exists?(user.id) || # user is admin
  25. 40 (poll.group.members_can_raise_motions && poll.group.members.exists?(user.id)) || # user is member
  26. 7 (poll.group.members_can_raise_motions && poll.discussion.present? && poll.discussion.guests.exists?(user.id)))
  27. )
  28. ) ||
  29. 10 (poll.group_id.nil? && poll.discussion_id && poll.discussion.members.exists?(user.id)) ||
  30. 10 (poll.group_id.nil? && poll.discussion_id.nil? && user.is_logged_in? && user.email_verified?)
  31. )
  32. end
  33. 1713 can [:announce, :remind], ::Poll do |poll|
  34. 38 if poll.group_id
  35. 35 poll.group.admins.exists?(user.id) ||
  36. 24 (poll.group.members_can_announce && poll.admins.exists?(user.id))
  37. else
  38. 3 poll.admins.exists?(user.id) ||
  39. 2 (!poll.specified_voters_only && poll.members.exists?(user.id))
  40. end
  41. end
  42. 1713 can [:add_voters, :add_members], ::Poll do |poll|
  43. 1073 poll.admins.exists?(user.id)
  44. end
  45. 1713 can [:add_guests], ::Poll do |poll|
  46. 932 if poll.group_id
  47. 911 Subscription.for(poll.group).allow_guests &&
  48. 911 (poll.group.admins.exists?(user.id) || (poll.group.members_can_add_guests && poll.admins.exists?(user.id)))
  49. else
  50. 21 poll.admins.exists?(user.id)
  51. end
  52. end
  53. 1713 can [:update], ::Poll do |poll|
  54. 23 (poll.discussion_id.blank? || !poll.discussion.closed_at) &&
  55. poll.admins.exists?(user.id)
  56. end
  57. 1713 can [:destroy], ::Poll do |poll|
  58. 3 (poll.discussion_id.blank? || !poll.discussion.closed_at) &&
  59. poll.admins.exists?(user.id)
  60. end
  61. 1713 can :close, ::Poll do |poll|
  62. 10 poll.active? &&
  63. poll.admins.exists?(user.id)
  64. end
  65. 1713 can :reopen, ::Poll do |poll|
  66. 4 poll.closed? &&
  67. !poll.anonymous &&
  68. can?(:update, poll)
  69. end
  70. end
  71. end

app/models/ability/poll_template.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 module Ability::PollTemplate
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can([:create, :update], ::PollTemplate) do |poll_template|
  5. poll_template.group.admins.exists?(user.id)
  6. end
  7. end
  8. end

app/models/ability/reaction.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 module Ability::Reaction
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::Reaction do |reaction|
  5. can?(:show, reaction.reactable)
  6. end
  7. 1713 can :update, ::Reaction do |reaction|
  8. 5 user.is_logged_in? && can?(:show, reaction.reactable)
  9. end
  10. 1713 can :destroy, ::Reaction do |reaction|
  11. 2 user_is_author_of?(reaction)
  12. end
  13. end
  14. end

app/models/ability/stance.rb

86.67% lines covered

15 relevant lines. 13 lines covered and 2 lines missed.
    
  1. 1 module Ability::Stance
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::Stance do |stance|
  5. user.can?(:show, stance.poll)
  6. end
  7. 1713 can [:update], ::Stance do |stance|
  8. 54 can?(:vote_in, stance.poll) &&
  9. stance.real_participant == user &&
  10. stance.latest?
  11. end
  12. 1713 can [:uncast], ::Stance do |stance|
  13. 2 can?(:update, stance) && stance.cast_at.present?
  14. end
  15. 1713 can [:create], ::Stance do |stance|
  16. 2 user.can? :vote_in, stance.poll
  17. end
  18. 1713 can :redeem, ::Stance do |stance|
  19. Stance.redeemable.exists?(stance.id)
  20. end
  21. 1713 can [:make_admin, :remove_admin, :resend, :remove], ::Stance do |stance|
  22. 6 stance.poll.admins.exists?(user.id)
  23. end
  24. end
  25. end

app/models/ability/tag.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. 1 module Ability
  2. 1 module Tag
  3. 1 def initialize(user)
  4. 1713 super(user)
  5. 1713 can [:create, :update, :destroy], ::Tag do |tag|
  6. 6 tag.group.parent_or_self.admins.exists? user.id
  7. end
  8. 1713 can :show, ::Tag do |tag|
  9. user.can? :show, tag.group
  10. end
  11. end
  12. end
  13. end

app/models/ability/task.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module Ability::Task
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can [:update], ::Task do |task|
  5. 5 (task.author_id == user.id) || can?(:update, task.record) || (task.users.exists? user.id)
  6. end
  7. end
  8. end

app/models/ability/user.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 module Ability::User
  2. 1 def initialize(user)
  3. 1713 super(user)
  4. 1713 can :show, ::User do |u|
  5. 4 user.is_logged_in? && u.deactivated_at.nil?
  6. end
  7. 1713 can [:update,
  8. :see_notifications_for,
  9. :subscribe_to], ::User do |u|
  10. 6 user == u
  11. end
  12. 1713 can [:deactivate], ::User do |u|
  13. 3 (user == u || user.is_admin?) && u.deactivated_at.nil?
  14. end
  15. 1713 can [:redact], ::User do |u|
  16. 4 (user == u || user.is_admin?) && !u.email.nil?
  17. end
  18. 1713 can [:contact], ::User do |u|
  19. 5 ContactableQuery.contactable(actor: user, user: u)
  20. end
  21. end
  22. end

app/models/anonymous_user.rb

72.73% lines covered

11 relevant lines. 8 lines covered and 3 lines missed.
    
  1. 1 class AnonymousUser < LoggedOutUser
  2. 1 def name
  3. 425 I18n.t(:'common.anonymous')
  4. end
  5. 1 def username
  6. :anonymous
  7. end
  8. 1 def name_or_username
  9. 9 name || username
  10. end
  11. 1 def avatar_kind
  12. 'initials'
  13. end
  14. 1 def avatar_initials
  15. "👤"
  16. end
  17. end

app/models/application_record.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class ApplicationRecord < ActiveRecord::Base
  2. self.abstract_class = true
  3. end

app/models/attachment.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class Attachment < ActiveStorage::Attachment
  2. end

app/models/blocked_domain.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class BlockedDomain < ApplicationRecord
  2. end

app/models/boot/site.rb

0.0% lines covered

38 relevant lines. 0 lines covered and 38 lines missed.
    
  1. module Boot
  2. class Site
  3. include LocalesHelper
  4. include Routing
  5. def payload
  6. @payload ||= {
  7. version: Loomio::Version.current,
  8. release: AppConfig.release,
  9. systemNotice: ENV['LOOMIO_SYSTEM_NOTICE'],
  10. environment: Rails.env,
  11. permittedParams: PermittedParamsSerializer.new({}),
  12. locales: ActiveModel::ArraySerializer.new(supported_locales, each_serializer: LocaleSerializer, root: false),
  13. defaultLocale: I18n.locale,
  14. newsletterEnabled: ENV['NEWSLETTER_ENABLED'],
  15. recaptchaKey: ENV['RECAPTCHA_APP_KEY'],
  16. baseUrl: root_url,
  17. contactEmail: ENV['SUPPORT_EMAIL'],
  18. plugins: { installed: [], outlets: [], routes: [] },
  19. theme: AppConfig.theme,
  20. sentry_dsn: ENV['SENTRY_PUBLIC_DSN'],
  21. plausible_src: ENV['PLAUSIBLE_SRC'],
  22. plausible_site: ENV['PLAUSIBLE_SITE'],
  23. features: {
  24. app: AppConfig.app_features
  25. },
  26. inlineTranslation: {
  27. isAvailable: TranslationService.available?
  28. },
  29. pollTypes: AppConfig.poll_types,
  30. pollColors: AppConfig.colors,
  31. webhookEventKinds: AppConfig.webhook_event_kinds,
  32. identityProviders: AppConfig.providers.fetch('identity', []).map do |provider|
  33. ({ name: provider, href: send("#{provider}_oauth_path") } if ENV["#{provider.upcase}_APP_KEY"])
  34. end.compact
  35. }
  36. end
  37. end
  38. end

app/models/boot/user.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module Boot
  2. 1 class User
  3. 1 attr_reader :user
  4. 1 def initialize(user, identity: {}, flash: {}, channel_token: nil)
  5. 6 @user = user
  6. 6 @identity = identity
  7. 6 @flash = flash.to_h
  8. 6 @channel_token = channel_token
  9. end
  10. 1 def payload
  11. 6 @payload ||= user_payload.merge(
  12. current_user_id: user.id,
  13. pending_identity: @identity,
  14. flash: @flash,
  15. channel_token: @channel_token
  16. )
  17. end
  18. 1 private
  19. 1 def user_payload
  20. 6 ActiveModel::ArraySerializer.new(Array(@user),
  21. 6 each_serializer: (user.restricted ? Restricted::UserSerializer : CurrentUserSerializer),
  22. root: :users
  23. ).as_json
  24. end
  25. end
  26. end

app/models/calendar_invite.rb

91.3% lines covered

23 relevant lines. 21 lines covered and 2 lines missed.
    
  1. 1 class CalendarInvite
  2. 1 include PrettyUrlHelper
  3. 1 include FormattedDateHelper
  4. 1 def initialize(outcome = Outcome.new)
  5. 8 @calendar = build_calendar(outcome)
  6. end
  7. 1 def to_ical
  8. 8 @calendar&.to_ical
  9. end
  10. 1 private
  11. 1 def build_calendar(outcome)
  12. 8 Icalendar::Calendar.new.tap do |calendar|
  13. 8 calendar.event do |event|
  14. 8 if outcome.poll_option.name.match /^\d+-\d+-\d+$/
  15. 8 event.duration = "+P0W1D0H0M"
  16. 8 event.dtstart = outcome.poll_option.name.to_date
  17. else
  18. event.duration = "+P0W0D0H#{outcome.poll.meeting_duration}M"
  19. event.dtstart = Icalendar::Values::DateTime.new(parse_datetime(outcome.poll_option.name), tzid: 'UTC')
  20. end
  21. 8 event.organizer = Icalendar::Values::CalAddress.new(outcome.author.email, cn: outcome.author.name)
  22. 8 event.summary = outcome.event_summary
  23. 8 event.description = outcome.event_description
  24. 8 event.location = outcome.event_location
  25. 8 event.attendee = outcome.attendee_emails
  26. 8 event.ip_class = "PRIVATE"
  27. 8 event.url = poll_url(outcome.poll)
  28. end
  29. end
  30. end
  31. end

app/models/chatbot.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 class Chatbot < ApplicationRecord
  2. 1 belongs_to :group
  3. 1 belongs_to :author, class_name: 'User'
  4. 1 validates_presence_of :server
  5. 1 validates_presence_of :name
  6. 1 validates_inclusion_of :kind, in: ['matrix', 'webhook']
  7. 1 validates_inclusion_of :webhook_kind, in: ['slack', 'microsoft', 'discord', 'markdown', nil]
  8. 1 def config
  9. {
  10. # id: self.id,
  11. server: self.server,
  12. access_token: self.access_token,
  13. channel: self.channel
  14. }
  15. end
  16. end

app/models/comment.rb

93.42% lines covered

76 relevant lines. 71 lines covered and 5 lines missed.
    
  1. 1 class Comment < ApplicationRecord
  2. 1 include Discard::Model
  3. 1 include CustomCounterCache::Model
  4. 1 include Translatable
  5. 1 include Reactable
  6. 1 include HasMentions
  7. 1 include HasCreatedEvent
  8. 1 include HasEvents
  9. 1 include HasRichText
  10. 1 include Searchable
  11. 1 def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
  12. 471 content_str = "regexp_replace(CONCAT_WS(' ', comments.body, users.name), E'<[^>]+>', '', 'gi')"
  13. 471 <<~SQL.squish
  14. INSERT INTO pg_search_documents (
  15. searchable_type,
  16. searchable_id,
  17. group_id,
  18. discussion_id,
  19. author_id,
  20. authored_at,
  21. content,
  22. ts_content,
  23. created_at,
  24. updated_at)
  25. SELECT 'Comment' AS searchable_type,
  26. comments.id AS searchable_id,
  27. discussions.group_id as group_id,
  28. discussions.id AS discussion_id,
  29. comments.user_id AS author_id,
  30. comments.created_at AS authored_at,
  31. #{content_str} AS content,
  32. to_tsvector('simple',#{content_str}) as ts_content,
  33. now() AS created_at,
  34. now() AS updated_at
  35. FROM comments
  36. LEFT JOIN discussions ON discussions.id = comments.discussion_id
  37. LEFT JOIN users ON users.id = comments.user_id
  38. #{discussion_id ? "LEFT JOIN events ON events.eventable_type = 'Comment' AND events.eventable_id = comments.id" : ""}
  39. WHERE comments.discarded_at IS NULL
  40. AND discussions.discarded_at IS NULL
  41. #{id ? " AND comments.id = #{id.to_i} LIMIT 1" : ""}
  42. #{author_id ? " AND comments.user_id = #{author_id.to_i}" : ""}
  43. #{discussion_id ? " AND events.discussion_id = #{discussion_id.to_i}" : ""}
  44. SQL
  45. end
  46. 1 has_paper_trail only: [:body, :body_format, :user_id, :discarded_at, :discarded_by]
  47. 1 is_translatable on: :body
  48. 1 is_mentionable on: :body
  49. 1 is_rich_text on: :body
  50. 1 belongs_to :discussion
  51. 1 belongs_to :user
  52. 1 belongs_to :parent, polymorphic: true
  53. 1 has_many :documents, as: :model, dependent: :destroy
  54. 1 validates_presence_of :user, unless: :discarded_at
  55. 1 validate :parent_comment_belongs_to_same_discussion
  56. 1 validate :has_body_or_attachment
  57. 1 alias_attribute :author, :user
  58. 1 alias_attribute :author_id, :user_id
  59. 1 scope :dangling, -> { joins('left join discussions on discussion_id = discussions.id').where('discussion_id is not null and discussions.id is null') }
  60. 1 scope :in_organisation, ->(group) { includes(:user, :discussion).joins(:discussion).where("discussions.group_id": group.id_and_subgroup_ids) }
  61. 1 before_validation :assign_parent_if_nil
  62. 1 delegate :name, to: :user, prefix: :user
  63. 1 delegate :name, to: :user, prefix: :author
  64. 1 delegate :email, to: :user, prefix: :user
  65. 1 delegate :author, to: :parent, prefix: :parent, allow_nil: true
  66. 1 delegate :group, to: :discussion
  67. 1 delegate :group_id, to: :discussion, allow_nil: true
  68. 1 delegate :full_name, to: :group, prefix: :group
  69. 1 delegate :locale, to: :user
  70. 1 delegate :mailer, to: :discussion
  71. 1 delegate :guests, to: :discussion
  72. 1 delegate :members, to: :discussion
  73. 1 delegate :title, to: :discussion
  74. 6 define_counter_cache(:versions_count) { |comment| comment.versions.count }
  75. 1 def real_participant
  76. author
  77. end
  78. 1 def assign_parent_if_nil
  79. 623 self.parent = self.discussion if self.parent_id.nil?
  80. end
  81. 1 def poll
  82. nil
  83. end
  84. 1 def poll_id
  85. nil
  86. end
  87. 1 def user
  88. 2488 super || AnonymousUser.new
  89. end
  90. 1 def should_pin
  91. 183 return false if body_format != "html"
  92. 2 Nokogiri::HTML(self.body).css("h1,h2,h3").length > 0
  93. end
  94. 1 def parent_event
  95. 246 if parent.nil? && discussion.present?
  96. self.parent = self.discussion
  97. save!(validate: false)
  98. end
  99. 246 if parent.is_a? Stance
  100. # if stance, the could be updated event. sucks i know
  101. Event.where(eventable_type: parent_type, eventable_id: parent_id).where('discussion_id is not null').first
  102. else
  103. 246 parent.created_event
  104. end
  105. end
  106. 1 def created_event_kind
  107. 42 :new_comment
  108. end
  109. 1 def is_most_recent?
  110. 2 discussion.comments.last == self
  111. end
  112. 1 def is_edited?
  113. edited_at.present?
  114. end
  115. 1 private
  116. 1 def has_body_or_attachment
  117. 623 if !discarded_at && body_blank? && files.empty? && image_files.empty?
  118. 3 errors.add(:body, I18n.t(:"activerecord.errors.messages.blank"))
  119. end
  120. end
  121. 1 def body_blank?
  122. 617 body.to_s.empty? || body.to_s == "<p></p>"
  123. end
  124. 1 def parent_comment_belongs_to_same_discussion
  125. # if someone replies to a deleted comment (in practice, by email), reparent to the discussion
  126. 623 self.parent = self.discussion if parent.nil? && discussion.present?
  127. 623 unless discussion_id == parent.discussion_id
  128. 6 errors.add(:parent, "Needs to have same discussion id")
  129. end
  130. end
  131. end

app/models/concerns/avatar_initials.rb

0.0% lines covered

15 relevant lines. 0 lines covered and 15 lines missed.
    
  1. module AvatarInitials
  2. # Requires base class to define:
  3. # avatar_initials
  4. # name
  5. # email
  6. # deactivated_at
  7. extend ActiveSupport::Concern
  8. def set_avatar_initials
  9. self.avatar_initials = get_avatar_initials[0..2]
  10. end
  11. def get_avatar_initials
  12. if deactivated_at
  13. "DU"
  14. elsif name.blank? || name == email
  15. email.to_s[0..1]
  16. else
  17. name.split.map(&:first).join
  18. end.upcase.gsub(/(\W|\d)/, "")
  19. end
  20. end

app/models/concerns/discussion_export_relations.rb

95.0% lines covered

20 relevant lines. 19 lines covered and 1 lines missed.
    
  1. 1 module DiscussionExportRelations
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 has_many :exportable_polls, -> { where("anonymous = false OR closed_at is not null") }, class_name: 'Poll', foreign_key: :group_id
  5. 1 has_many :exportable_poll_options, through: :exportable_polls, source: :poll_options
  6. 1 has_many :exportable_outcomes, through: :exportable_polls, source: :outcomes
  7. 1 has_many :exportable_stances, through: :exportable_polls, source: :stances
  8. 1 has_many :exportable_stance_choices, through: :exportable_stances, source: :stance_choices
  9. 1 has_many :comment_files, through: :comments, source: :files_attachments
  10. 1 has_many :comment_image_files, through: :comments, source: :image_files_attachments
  11. 1 has_many :poll_files, through: :exportable_polls, source: :files_attachments
  12. 1 has_many :poll_image_files, through: :exportable_polls, source: :image_files_attachments
  13. 1 has_many :outcome_files, through: :exportable_outcomes, source: :files_attachments
  14. 1 has_many :outcome_image_files, through: :exportable_outcomes, source: :image_files_attachments
  15. 1 has_many :exportable_poll_reactions, -> { joins(:user) }, through: :exportable_polls, source: :reactions
  16. 1 has_many :exportable_stance_reactions, -> { joins(:user) }, through: :exportable_stances, source: :reactions
  17. 1 has_many :comment_reactions, -> { joins(:user) }, through: :comments, source: :reactions
  18. 1 has_many :exportable_outcome_reactions, -> { joins(:user) }, through: :exportable_outcomes, source: :reactions
  19. end
  20. 1 def all_reactions
  21. Queries::UnionQuery.for(:reactions, [
  22. self.reactions,
  23. self.exportable_poll_reactions,
  24. self.exportable_stance_reactions,
  25. self.comment_reactions,
  26. self.exportable_outcome_reactions
  27. ])
  28. end
  29. end

app/models/concerns/events/live_update.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module Events::LiveUpdate
  2. 1 def trigger!
  3. 850 super
  4. 850 notify_clients!
  5. end
  6. # send client live updates
  7. 1 def notify_clients!
  8. 850 return unless eventable
  9. 850 if eventable.group_id
  10. 835 MessageChannelService.publish_models([self], group_id: eventable.group.id)
  11. end
  12. 850 if eventable.respond_to?(:guests)
  13. 568 eventable.guests.find_each do |user|
  14. 16 MessageChannelService.publish_models([self], user_id: user.id)
  15. end
  16. end
  17. end
  18. end

app/models/concerns/events/notify/author.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 1 module Events::Notify::Author
  2. 1 def trigger!
  3. 50 super
  4. 50 email_author!
  5. end
  6. 1 def email_author!
  7. 50 EventMailer.event(author, self).deliver_later if notify_author?
  8. end
  9. 1 private
  10. 1 def author
  11. 43 eventable.author
  12. end
  13. # override if we want to send to the author conditionally
  14. 1 def notify_author?
  15. true
  16. end
  17. end

app/models/concerns/events/notify/by_email.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 module Events::Notify::ByEmail
  2. 1 def trigger!
  3. 941 super
  4. 941 email_users!
  5. end
  6. # send event emails
  7. 1 def email_users!
  8. 940 email_recipients.active.uniq.pluck(:id).each do |recipient_id|
  9. 965 EventMailer.event(recipient_id, self.id).deliver_later
  10. end
  11. end
  12. 1 def wait_time
  13. 1.minute
  14. end
  15. end

app/models/concerns/events/notify/chatbots.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Events::Notify::Chatbots
  2. 1 def trigger!
  3. 901 super
  4. 901 GenericWorker.set(wait_until: 30.seconds).perform_async('ChatbotService', 'publish_event!', self.id)
  5. end
  6. end

app/models/concerns/events/notify/in_app.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 module Events::Notify::InApp
  2. 1 include PrettyUrlHelper
  3. 1 def trigger!
  4. 792 super
  5. 792 self.notify_users!
  6. end
  7. # send event notifications
  8. 1 def notify_users!
  9. 792 notifications.import(built_notifications)
  10. 1730 built_notifications.each { |n| MessageChannelService.publish_models(Array(n), user_id: n.user_id) }
  11. end
  12. 1 private
  13. 1 def built_notifications
  14. 2522 @built ||= notification_recipients.active.map { |recipient| notification_for(recipient) }
  15. end
  16. 1 def notification_for(recipient)
  17. 956 I18n.with_locale(recipient.locale) do
  18. 956 notifications.build(
  19. user: recipient,
  20. actor: notification_actor,
  21. url: notification_url,
  22. translation_values: notification_translation_values
  23. )
  24. end
  25. end
  26. # defines the avatar which appears next to the notification
  27. 1 def notification_actor
  28. 1898 user.presence
  29. end
  30. # defines the link that clicking on the notification takes you to
  31. 1 def notification_url
  32. 951 polymorphic_path(eventable)
  33. end
  34. # defines the values that are passed to the translation for notification text
  35. # by default we infer the values needed from the eventable class,
  36. # but this method can be overridden with any translation values for a particular event
  37. 1 def notification_translation_values
  38. {
  39. 954 name: notification_translation_name,
  40. title: notification_translation_title,
  41. 954 poll_type: (I18n.t(:"poll_types.#{notification_poll_type}") if notification_poll_type)
  42. }.compact
  43. end
  44. 1 def notification_translation_name
  45. 954 notification_actor&.name
  46. end
  47. 1 def notification_translation_title
  48. 954 polymorphic_title(eventable)
  49. end
  50. 1 def notification_poll_type
  51. 1843 eventable.poll_type if eventable.respond_to?(:poll_type)
  52. end
  53. end

app/models/concerns/events/notify/mentions.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. 1 module Events::Notify::Mentions
  2. 1 def trigger!
  3. 829 super
  4. 829 self.notify_mentions!
  5. end
  6. # send event notifications
  7. 1 def notify_mentions!
  8. 798 return unless eventable.newly_mentioned_users.any?
  9. 27 if eventable.respond_to?(:discussion) && eventable.discussion.present?
  10. 27 eventable.newly_mentioned_users.each do |guest|
  11. 27 if !eventable.group.members.exists?(guest.id)
  12. eventable.discussion.add_guest!(guest, user)
  13. end
  14. end
  15. end
  16. 27 Events::UserMentioned.publish! eventable, user, eventable.newly_mentioned_users
  17. end
  18. 1 private
  19. # remove newly_mentioned_users from those emailed by following
  20. 1 def email_recipients
  21. 573 super.where.not(id: eventable.newly_mentioned_users)
  22. end
  23. end

app/models/concerns/group_export_relations.rb

0.0% lines covered

102 relevant lines. 0 lines covered and 102 lines missed.
    
  1. module GroupExportRelations
  2. extend ActiveSupport::Concern
  3. included do
  4. #tags
  5. has_many :tags
  6. # polls
  7. has_many :exportable_polls, -> { where("anonymous = false OR closed_at is not null") }, class_name: 'Poll', foreign_key: :group_id
  8. has_many :discussion_taggings, through: :discussions, source: :taggings
  9. has_many :poll_taggings, through: :exportable_polls, source: :taggings
  10. has_many :exportable_poll_options, through: :exportable_polls, source: :poll_options
  11. has_many :exportable_outcomes, through: :exportable_polls, source: :outcomes
  12. has_many :exportable_stances, through: :exportable_polls, source: :stances
  13. has_many :exportable_stance_choices, through: :exportable_stances, source: :stance_choices
  14. # attachments
  15. has_many :comment_files, through: :comments, source: :files_attachments
  16. has_many :comment_image_files, through: :comments, source: :image_files_attachments
  17. has_many :discussion_files, through: :discussions, source: :files_attachments
  18. has_many :discussion_image_files, through: :discussions, source: :image_files_attachments
  19. has_many :poll_files, through: :exportable_polls, source: :files_attachments
  20. has_many :poll_image_files, through: :exportable_polls, source: :image_files_attachments
  21. has_many :outcome_files, through: :exportable_outcomes, source: :files_attachments
  22. has_many :outcome_image_files, through: :exportable_outcomes, source: :image_files_attachments
  23. has_many :subgroup_files, through: :subgroups, source: :files_attachments
  24. has_many :subgroup_image_files, through: :subgroups, source: :image_files_attachments
  25. has_many :subgroup_cover_photos, through: :subgroups, source: :cover_photo_attachment
  26. has_many :subgroup_logos, through: :subgroups, source: :logo_attachment
  27. # documents
  28. has_many :discussion_documents, through: :discussions, source: :documents
  29. has_many :exportable_poll_documents, through: :exportable_polls, source: :documents
  30. has_many :comment_documents, through: :comments, source: :documents
  31. has_many :public_discussion_documents, through: :public_discussions, source: :documents
  32. has_many :public_comment_documents, through: :public_comments, source: :documents
  33. # reactions
  34. has_many :discussion_reactions, -> { joins(:user) }, through: :discussions, source: :reactions
  35. has_many :exportable_poll_reactions, -> { joins(:user) }, through: :exportable_polls, source: :reactions
  36. has_many :exportable_stance_reactions, -> { joins(:user) }, through: :exportable_stances, source: :reactions
  37. has_many :comment_reactions, -> { joins(:user) }, through: :comments, source: :reactions
  38. has_many :exportable_outcome_reactions, -> { joins(:user) }, through: :exportable_outcomes, source: :reactions
  39. # readers
  40. has_many :discussion_readers, through: :discussions
  41. # users
  42. has_many :discussion_authors, through: :discussions, source: :author
  43. has_many :comment_authors, through: :comments, source: :user
  44. has_many :exportable_poll_authors, through: :exportable_polls, source: :author
  45. has_many :exportable_outcome_authors, through: :exportable_outcomes, source: :author
  46. has_many :exportable_stance_authors, through: :exportable_stances, source: :participant
  47. has_many :reader_users, through: :discussion_readers, source: :user
  48. # events
  49. has_many :membership_events, through: :memberships, source: :events
  50. has_many :discussion_events, through: :discussions, source: :events
  51. has_many :comment_events, through: :comments, source: :events
  52. has_many :exportable_poll_events, through: :exportable_polls, source: :events
  53. has_many :exportable_outcome_events, through: :exportable_outcomes, source: :events
  54. has_many :exportable_stance_events, through: :exportable_stances, source: :events
  55. end
  56. def all_users
  57. Queries::UnionQuery.for(:users, [
  58. self.members,
  59. self.discussion_authors,
  60. self.comment_authors,
  61. self.exportable_poll_authors,
  62. self.exportable_outcome_authors,
  63. self.exportable_stance_authors,
  64. self.reaction_users,
  65. self.reader_users
  66. ])
  67. end
  68. # def related_attachments
  69. # Queries::UnionQuery.for(:attachments, [
  70. # self.comment_files,
  71. # self.comment_image_files,
  72. # self.discussion_files,
  73. # self.discussion_image_files,
  74. # self.poll_files,
  75. # self.poll_image_files,
  76. # self.outcome_files,
  77. # self.outcome_image_files,
  78. # self.subgroup_files,
  79. # self.subgroup_image_files,
  80. # self.subgroup_cover_photos,
  81. # self.subgroup_logos,
  82. # ])
  83. # end
  84. def all_taggings
  85. Queries::UnionQuery.for(:taggings, [
  86. self.discussion_taggings,
  87. self.poll_taggings
  88. ])
  89. end
  90. def all_groups
  91. Group.where(id: id_and_subgroup_ids)
  92. end
  93. def all_events
  94. Queries::UnionQuery.for(:events, [
  95. self.membership_events,
  96. self.discussion_events,
  97. self.comment_events,
  98. self.exportable_poll_events,
  99. self.exportable_outcome_events,
  100. self.exportable_stance_events
  101. ])
  102. end
  103. def all_notifications
  104. Notification.where(event_id: all_events.pluck(:id))
  105. end
  106. def all_documents
  107. Queries::UnionQuery.for(:documents, [
  108. self.documents,
  109. self.discussion_documents,
  110. self.exportable_poll_documents,
  111. self.comment_documents
  112. ])
  113. end
  114. def all_reactions
  115. Queries::UnionQuery.for(:reactions, [
  116. self.discussion_reactions,
  117. self.exportable_poll_reactions,
  118. self.exportable_stance_reactions,
  119. self.comment_reactions,
  120. self.exportable_outcome_reactions
  121. ])
  122. end
  123. def reaction_users
  124. User.where(id: all_reactions.pluck(:user_id))
  125. end
  126. end

app/models/concerns/group_privacy.rb

0.0% lines covered

132 relevant lines. 0 lines covered and 132 lines missed.
    
  1. module GroupPrivacy
  2. extend ActiveSupport::Concern
  3. DISCUSSION_PRIVACY_OPTIONS = ['public_only', 'private_only', 'public_or_private'].freeze
  4. MEMBERSHIP_GRANTED_UPON_OPTIONS = ['request', 'approval', 'invitation'].freeze
  5. included do
  6. after_initialize :set_privacy_defaults
  7. before_validation :set_discussions_private_only, if: :is_hidden_from_public?
  8. validate :validate_parent_members_can_see_discussions
  9. validate :validate_is_visible_to_parent_members
  10. validate :validate_discussion_privacy_options
  11. validate :validate_trial_group_cannot_be_public
  12. validates_inclusion_of :discussion_privacy_options, in: DISCUSSION_PRIVACY_OPTIONS
  13. validates_inclusion_of :membership_granted_upon, in: MEMBERSHIP_GRANTED_UPON_OPTIONS
  14. end
  15. # this method's a bit chunky. New class?
  16. def group_privacy=(term)
  17. case term
  18. when 'open'
  19. self.is_visible_to_public = true
  20. self.discussion_privacy_options = 'public_only'
  21. self.listed_in_explore = true
  22. unless %w[approval request invitation].include?(self.membership_granted_upon)
  23. self.membership_granted_upon = 'approval'
  24. end
  25. when 'closed'
  26. self.is_visible_to_public = true
  27. self.membership_granted_upon = 'approval'
  28. self.listed_in_explore = false
  29. unless %w[private_only public_or_private].include?(self.discussion_privacy_options)
  30. self.discussion_privacy_options = 'private_only'
  31. end
  32. # closed subgroup of hidden parent means parent members can seeee it!
  33. if is_subgroup_of_hidden_parent?
  34. self.is_visible_to_parent_members = true
  35. self.is_visible_to_public = false
  36. end
  37. when 'secret'
  38. self.is_visible_to_public = false
  39. self.listed_in_explore = false
  40. self.discussion_privacy_options = 'private_only'
  41. self.membership_granted_upon = 'invitation'
  42. self.is_visible_to_parent_members = false
  43. else
  44. raise "group_privacy term not recognised: #{term}"
  45. end
  46. end
  47. def group_privacy
  48. if is_visible_to_public?
  49. self.public_discussions_only? ? 'open' : 'closed'
  50. elsif parent_id && is_visible_to_parent_members?
  51. 'closed'
  52. else
  53. 'secret'
  54. end
  55. end
  56. def is_hidden_from_public?
  57. !is_visible_to_public?
  58. end
  59. def private_discussions_only?
  60. discussion_privacy_options == 'private_only'
  61. end
  62. def public_discussions_only?
  63. discussion_privacy_options == 'public_only'
  64. end
  65. def public_or_private_discussions_allowed?
  66. discussion_privacy_options == 'public_or_private'
  67. end
  68. def membership_granted_upon_approval?
  69. membership_granted_upon == 'approval'
  70. end
  71. def membership_granted_upon_request?
  72. membership_granted_upon == 'request'
  73. end
  74. def membership_granted_upon_invitation?
  75. membership_granted_upon == 'invitation'
  76. end
  77. def discussion_private_default
  78. self.discussion_privacy_options != "public_only"
  79. end
  80. def set_discussions_private_only
  81. self.discussion_privacy_options = 'private_only'
  82. end
  83. def validate_discussion_privacy_options
  84. unless is_visible_to_parent_members?
  85. if group_privacy == 'open' and !public_discussions_only?
  86. self.errors.add(:discussion_privacy_options, "Discussions must be public if group is open")
  87. end
  88. end
  89. if is_hidden_from_public? and not private_discussions_only?
  90. self.errors.add(:discussion_privacy_options, "Discussions must be private if group is hidden")
  91. end
  92. end
  93. def validate_parent_members_can_see_discussions
  94. self.errors.add(:parent_members_can_see_discussions) unless parent_members_can_see_discussions_is_valid?
  95. end
  96. def validate_is_visible_to_parent_members
  97. self.errors.add(:is_visible_to_parent_members) unless visible_to_parent_members_is_valid?
  98. end
  99. def validate_trial_group_cannot_be_public
  100. if !self.parent_id &&
  101. self.subscription &&
  102. self.subscription.plan == 'trial' &&
  103. self.is_visible_to_public
  104. self.errors.add(:group_privacy, I18n.t('group.error.no_public_trials'))
  105. end
  106. end
  107. def parent_members_can_see_discussions_is_valid?
  108. if is_visible_to_public?
  109. true
  110. else
  111. if parent_members_can_see_discussions?
  112. is_visible_to_parent_members?
  113. else
  114. true
  115. end
  116. end
  117. end
  118. def visible_to_parent_members_is_valid?
  119. if is_visible_to_public?
  120. true
  121. else
  122. if is_visible_to_parent_members?
  123. is_hidden_from_public? and is_subgroup?
  124. else
  125. true
  126. end
  127. end
  128. end
  129. def set_privacy_defaults
  130. self.is_visible_to_public ||= false
  131. self.discussion_privacy_options ||= 'private_only'
  132. self.membership_granted_upon ||= 'approval'
  133. end
  134. end

app/models/concerns/has_avatar.rb

0.0% lines covered

75 relevant lines. 0 lines covered and 75 lines missed.
    
  1. module HasAvatar
  2. include AvatarInitials
  3. include Routing
  4. extend ActiveSupport::Concern
  5. included do
  6. include Gravtastic
  7. gravtastic rating: :pg, default: :none
  8. before_create :set_default_avatar_kind
  9. end
  10. def set_default_avatar_kind
  11. if uploaded_avatar.attached?
  12. self.avatar_kind = :uploaded
  13. elsif has_gravatar?
  14. self.avatar_kind = :gravatar
  15. else
  16. self.avatar_kind = :initials
  17. end
  18. end
  19. def avatar_kind
  20. return 'mdi-duck' if deactivated_at?
  21. return 'mdi-email-outline' if !name
  22. super
  23. end
  24. def thumb_url
  25. avatar_url(128)
  26. end
  27. def avatar_url(size = 512)
  28. if avatar_kind == 'uploaded' && (!uploaded_avatar.attached? or uploaded_avatar.attachment.nil?)
  29. update_columns(avatar_kind: set_default_avatar_kind)
  30. end
  31. case avatar_kind
  32. when 'gravatar'
  33. gravatar_url(size: size, secure: true, default: 'retro')
  34. when 'uploaded'
  35. uploaded_avatar_url(size)
  36. else
  37. nil
  38. end
  39. rescue ActiveStorage::UnrepresentableError
  40. update_columns(avatar_kind: :initials)
  41. nil
  42. end
  43. def avatar_initials_url(size = 256)
  44. colors = AppConfig.theme[:brand_colors].slice(:gold, :sky, :wellington, :sunset).values
  45. color = colors[id % colors.length].gsub('#','')
  46. params = {
  47. name: String(avatar_initials).split('').join('+'),
  48. background: colors[id % colors.length].gsub('#',''),
  49. color: '000000',
  50. rounded: true,
  51. format: :png,
  52. size: size
  53. }
  54. "https://ui-avatars.com/api/?#{params.to_a.map{|p| p.join('=')}.join('&')}"
  55. end
  56. def uploaded_avatar_url(size = 512)
  57. size = size.to_i
  58. return unless uploaded_avatar.attached?
  59. Rails.application.routes.url_helpers.rails_representation_path(
  60. uploaded_avatar.representation(resize_to_limit: [size,size], saver: {quality: 80, strip: true}),
  61. only_path: true
  62. )
  63. end
  64. def has_gravatar?(options = {})
  65. return false if Rails.env.test?
  66. hash = Digest::MD5.hexdigest(email.to_s.downcase)
  67. options = { :rating => 'x', :timeout => 2 }.merge(options)
  68. http = Net::HTTP.new('www.gravatar.com', 80)
  69. http.read_timeout = options[:timeout]
  70. response = http.request_head("/avatar/#{hash}?rating=#{options[:rating]}&default=http://gravatar.com/avatar")
  71. response.code != '302'
  72. rescue StandardError, Timeout::Error
  73. false # Don't show "gravatar" if the service is down or slow
  74. end
  75. end

app/models/concerns/has_created_event.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. module HasCreatedEvent
  2. def created_event
  3. events.find_by(kind: created_event_kind)
  4. end
  5. def created_event_kind
  6. :"#{self.class.name.downcase}_created"
  7. end
  8. def create_missing_created_event!
  9. self.events.create(kind: created_event_kind, user_id: author_id, created_at: created_at)
  10. end
  11. end

app/models/concerns/has_custom_fields.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module HasCustomFields
  2. def set_custom_fields(*fields)
  3. fields.map(&:to_s).each do |field|
  4. define_method field, -> { self[:custom_fields][field] }
  5. define_method :"#{field}=", ->(value) { self[:custom_fields][field] = value }
  6. end
  7. end
  8. end

app/models/concerns/has_defaults.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module HasDefaults
  2. def initialized_with_default(column, method = nil)
  3. after_initialize do
  4. send(:"#{column}=", method&.call) if send(column) == nil
  5. end
  6. end
  7. end

app/models/concerns/has_events.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module HasEvents
  2. extend ActiveSupport::Concern
  3. included do
  4. has_many :events, -> { includes :user, :eventable }, as: :eventable, dependent: :destroy
  5. has_many :notifications, through: :events
  6. has_many :users_notified, -> { distinct }, through: :notifications, source: :user
  7. end
  8. end

app/models/concerns/has_experiences.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. module HasExperiences
  2. def experienced!(key, toggle = true)
  3. experiences[key] = toggle
  4. save
  5. end
  6. end

app/models/concerns/has_mentions.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. module HasMentions
  2. extend ActiveSupport::Concern
  3. include Twitter::Extractor
  4. include HasEvents
  5. module ClassMethods
  6. def is_mentionable(on: [])
  7. define_singleton_method :mentionable_fields, -> { Array on }
  8. end
  9. end
  10. def mentioned_usernames
  11. if text_format == "md"
  12. extract_mentioned_screen_names(mentionable_text).uniq - [self.author&.username]
  13. else
  14. Nokogiri::HTML::fragment(mentionable_text).search("span[data-mention-id]").map do |el|
  15. el['data-mention-id']
  16. end.filter { |id_or_username| id_or_username.to_i.to_s != id_or_username }
  17. end
  18. end
  19. def mentioned_user_ids
  20. # html text could use ids or usernames depending on the age of the content
  21. return [] if text_format == "md"
  22. Nokogiri::HTML::fragment(mentionable_text).search("span[data-mention-id]").map do |el|
  23. el['data-mention-id']
  24. end.filter { |id_or_username| id_or_username.to_i.to_s == id_or_username }
  25. end
  26. def mentioned_users
  27. members.where("users.username in (:usernames) or users.id in (:ids)", usernames: mentioned_usernames, ids: mentioned_user_ids)
  28. end
  29. # users mentioned in the text, but not yet sent notifications
  30. def newly_mentioned_users
  31. mentioned_users
  32. .where.not(id: already_mentioned_users) # avoid re-mentioning users when editing
  33. .where.not(id: users_to_not_mention)
  34. end
  35. # users mentioned on a previous edit of this model
  36. def already_mentioned_users
  37. User.where(id: self.notifications.user_mentions.pluck(:user_id))
  38. end
  39. def users_to_not_mention
  40. User.none # overridden with specific users to not receive mentions
  41. end
  42. private
  43. def text_format
  44. self.send("#{self.class.mentionable_fields.first}_format")
  45. end
  46. def mentionable_text
  47. self.class.mentionable_fields.map { |field| self.send(field) }.join('|')
  48. end
  49. end

app/models/concerns/has_rich_text.rb

0.0% lines covered

118 relevant lines. 0 lines covered and 118 lines missed.
    
  1. module HasRichText
  2. PREVIEW_OPTIONS = {
  3. resize_to_limit: [1280,1280],
  4. saver: {
  5. quality: 85,
  6. strip: true
  7. }
  8. }
  9. extend ActiveSupport::Concern
  10. module ClassMethods
  11. def is_rich_text(on: [])
  12. define_singleton_method :rich_text_fields, -> { Array on }
  13. rich_text_fields.each do |field|
  14. define_method "sanitize_#{field}!" do
  15. # return if self.send("#{field}_format") == 'md'
  16. tags = %w[strong em b i p s code pre big div small hr br span mark h1 h2 h3 ul ol li abbr a img video audio blockquote table thead th tr td iframe u]
  17. attributes = %w[href src alt title data-type data-iframe-container data-done data-mention-id poster controls data-author-id data-uid data-checked data-due-on data-color data-remind width height target colspan rowspan data-text-align]
  18. self[field] = Rails::Html::WhiteListSanitizer.new.sanitize(self[field], tags: tags, attributes: attributes)
  19. self[field] = HasRichText::strip_empty_paragraphs(self[field])
  20. self[field] = add_required_link_attributes(self[field])
  21. self[field] = HasRichText::add_heading_ids(self[field])
  22. self[field] = TaskService.rewrite_uids(self[field])
  23. # preserve markdown quotes after html sanitizer
  24. self[field] = self[field].gsub(/^&gt\; /, '> ') if self.send("#{field}_format") == 'md'
  25. end
  26. define_method "#{field}_visible_text" do
  27. if self.send("#{field}_format") == 'html'
  28. Nokogiri::HTML(self[field]).text
  29. else
  30. Nokogiri::HTML(MarkdownService.render_html(self[field])).text
  31. end
  32. end
  33. define_method "body_is_blank?" do
  34. self[field] == '' ||
  35. self[field] == nil ||
  36. self[field] == '<p></p>'
  37. end
  38. before_save :"sanitize_#{field}!"
  39. define_method "parse_and_update_tasks_#{field}!" do
  40. TaskService.parse_and_update(self, field)
  41. end
  42. after_save :"parse_and_update_tasks_#{field}!"
  43. validates field, {length: {maximum: Rails.application.secrets.max_message_length}}
  44. validates_inclusion_of :"#{field}_format", in: ['html', 'md']
  45. if respond_to?(:after_discard)
  46. after_discard do
  47. tasks.discard_all
  48. end
  49. after_undiscard do
  50. tasks.undiscard_all
  51. end
  52. end
  53. end
  54. end
  55. end
  56. included do
  57. has_many_attached :files, dependent: :detach
  58. has_many_attached :image_files, dependent: :detach
  59. has_many :tasks, as: :record
  60. before_save :update_content_locale
  61. before_save :build_attachments
  62. before_save :sanitize_link_previews
  63. end
  64. def update_content_locale
  65. return unless self.changed.intersection(self.class.rich_text_fields.map(&:to_s)).any?
  66. combined_text = self.class.rich_text_fields.map {|field| self[field] }.join(' ')
  67. stripped_text = Rails::Html::WhiteListSanitizer.new.sanitize(combined_text, tags: [])
  68. result = CLD.detect_language stripped_text
  69. self.content_locale = result[:code] if result[:reliable]
  70. end
  71. def sanitize_link_previews
  72. sanitizer = Rails::Html::FullSanitizer.new
  73. self.link_previews.each do |preview|
  74. preview.keys.each do |key|
  75. preview[key] = String(sanitizer.sanitize preview[key]).truncate(480)
  76. end
  77. end
  78. end
  79. def build_attachments
  80. # this line is just to help migrations through
  81. return true unless self.class.column_names.include?('attachments')
  82. self[:attachments] = files.map do |file|
  83. i = file.blob.slice(:id, :filename, :content_type, :byte_size)
  84. i.merge!({ preview_url: Rails.application.routes.url_helpers.rails_representation_path(file.representation(PREVIEW_OPTIONS), only_path: true) }) if file.representable?
  85. i.merge!({ download_url: Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) })
  86. i.merge!({ icon: attachment_icon(file.content_type || file.filename) })
  87. i.merge!({ signed_id: file.signed_id })
  88. i
  89. end
  90. end
  91. def assign_attributes_and_files(params)
  92. self.assign_attributes API::V1::SnorlaxBase.filter_params(self.class, params)
  93. end
  94. def attachment_icon(name)
  95. AppConfig.doctypes.detect{ |type| /#{type['regex']}/.match(name) }['icon']
  96. end
  97. def self.strip_empty_paragraphs(text)
  98. fragment = Nokogiri::HTML::DocumentFragment.parse(text)
  99. fragment.css('p').each do |node|
  100. if node.content.match?(/^[[:space:]]+$/)
  101. node.content = node.content.gsub(/^[[:space:]]+$/, '')
  102. end
  103. end
  104. fragment.to_s
  105. end
  106. def self.add_heading_ids(text)
  107. fragment = Nokogiri::HTML::DocumentFragment.parse(text)
  108. fragment.css('h1,h2,h3,h4,h5,h6').each do |node|
  109. node['id'] = node.text[0,60].strip.parameterize
  110. end
  111. fragment.to_s
  112. end
  113. def add_required_link_attributes(text)
  114. fragment = Nokogiri::HTML::DocumentFragment.parse(text)
  115. fragment.css('a').each do |node|
  116. node['rel'] = 'nofollow ugc noreferrer noopener'
  117. node['target'] = '_blank'
  118. end
  119. fragment.to_s
  120. end
  121. end

app/models/concerns/has_tags.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. module HasTags
  2. extend ActiveSupport::Concern
  3. included do
  4. after_save :update_group_tags
  5. end
  6. def tag_models
  7. group.tags.where(name: self.tags).order(:priority)
  8. end
  9. def update_group_tags
  10. return unless self.group_id
  11. GenericWorker.perform_async('TagService', 'update_group_and_org_tags', self.group_id)
  12. end
  13. end

app/models/concerns/has_timeframe.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. module HasTimeframe
  2. extend ActiveSupport::Concern
  3. included do
  4. scope :within, ->(since, till, field = nil) { where("#{self.table_name}.#{field || :created_at} BETWEEN ? AND ?", since || 100.years.ago, till || 100.years.from_now) }
  5. scope :until, ->(till) { within(nil, till) }
  6. scope :since, ->(since) { within(since, nil) }
  7. def self.has_timeframe?
  8. true
  9. end
  10. end
  11. end

app/models/concerns/has_tokens.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module HasTokens
  2. def initialized_with_token(column, method = nil)
  3. after_initialize do
  4. send(:"#{column}=", send(column) || method&.call || self.class.generate_unique_secure_token) if self.respond_to?("#{column}=")
  5. end
  6. end
  7. end

app/models/concerns/has_volume.rb

0.0% lines covered

34 relevant lines. 0 lines covered and 34 lines missed.
    
  1. module HasVolume
  2. extend ActiveSupport::Concern
  3. included do
  4. enum volume: {mute: 0, quiet: 1, normal: 2, loud: 3}
  5. scope :volume, ->(volume) { where(volume: volumes[volume]) }
  6. scope :volume_at_least, ->(volume) { where('volume >= ?', volumes[volume]) }
  7. scope :email_notifications, -> { where('volume >= ?', volumes[:normal]) }
  8. scope :app_notifications, -> { where('volume >= ?', volumes[:quiet]) }
  9. end
  10. def set_volume!(volume, persist: true)
  11. if self.class.volumes.include?(volume)
  12. self.volume = volume
  13. save if persist
  14. else
  15. self.errors.add :volume, I18n.t(:"activerecord.errors.messages.invalid")
  16. false
  17. end
  18. end
  19. def volume_is_normal_or_loud?
  20. volume_is_normal? || volume_is_loud?
  21. end
  22. def volume_is_loud?
  23. volume.to_s == 'loud'
  24. end
  25. def volume_is_normal?
  26. volume.to_s == 'normal'
  27. end
  28. def volume_is_quiet?
  29. volume.to_s == 'quiet'
  30. end
  31. def volume_is_mute?
  32. volume.to_s == 'mute'
  33. end
  34. end

app/models/concerns/identities/with_client.rb

0.0% lines covered

25 relevant lines. 0 lines covered and 25 lines missed.
    
  1. module Identities::WithClient
  2. def notify!(event)
  3. return unless valid_event_kinds.include?(event.kind)
  4. I18n.with_locale(event.group.locale) { client.post_content!(event) }
  5. end
  6. def fetch_user_info
  7. apply_user_info(client.fetch_user_info.json)
  8. end
  9. private
  10. def valid_event_kinds
  11. [
  12. 'new_discussion',
  13. 'poll_created',
  14. 'poll_closing_soon',
  15. 'poll_expired',
  16. 'outcome_created'
  17. ]
  18. end
  19. def client
  20. @client ||= "Clients::#{identity_type.to_s.classify}".constantize.new(token: self.access_token)
  21. end
  22. # called by default immediately after an access token is obtained.
  23. # Define a method here to get some basic information about the user,
  24. # like name, email, profile image, etc
  25. def apply_user_info(payload)
  26. raise NotImplementedError.new
  27. end
  28. end

app/models/concerns/message_channel.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. module MessageChannel
  2. extend ActiveSupport::Concern
  3. def message_channel
  4. "/#{self.class.to_s.downcase}-#{self.key}"
  5. end
  6. end

app/models/concerns/no_forbidden_emails.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module NoForbiddenEmails
  2. extend ActiveSupport::Concern
  3. FORBIDDEN_EMAIL_ADDRESSES = [ENV.fetch('DECIDE_EMAIL', "decide@#{ENV['CANONICAL_HOST']}")]
  4. included do
  5. validates_exclusion_of :email, in: FORBIDDEN_EMAIL_ADDRESSES
  6. end
  7. end

app/models/concerns/no_spam.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module NoSpam
  2. # eg:
  3. # SPAM_REGEX="(diide\.com|gusronk\.com|appnox\.com|akxpert\.com)"
  4. SPAM_REGEX = Regexp.new(ENV.fetch('SPAM_REGEX', "(diide\.com|gusronk\.com)"), 'i')
  5. def no_spam_for(*fields)
  6. Array(fields).each do |field|
  7. validates field, format: { without: SPAM_REGEX, message: "no spam" }
  8. end
  9. end
  10. end

app/models/concerns/null/group.rb

72.73% lines covered

44 relevant lines. 32 lines covered and 12 lines missed.
    
  1. 1 module Null::Group
  2. 1 include Null::Object
  3. 1 def group
  4. self
  5. end
  6. 1 alias :read_attribute_for_serialization :send
  7. 1 def new_threads_max_depth
  8. 2
  9. end
  10. 1 def new_threads_newest_first
  11. false
  12. end
  13. 1 def full_name
  14. I18n.t('discussion.direct_thread')
  15. end
  16. 1 def save
  17. 4 true
  18. end
  19. 1 def name
  20. I18n.t('discussion.direct_thread')
  21. end
  22. 1 def nil_methods
  23. %w(
  24. 2795 parent
  25. id
  26. key
  27. locale
  28. update_polls_count
  29. update_closed_polls_count
  30. update_discussions_count
  31. update_public_discussions_count
  32. update_open_discussions_count
  33. update_closed_discussions_count
  34. update_discussion_templates_count
  35. presence
  36. present?
  37. content_locale
  38. handle
  39. description
  40. description_format
  41. group_id
  42. add_member!
  43. message_channel
  44. logo_or_parent_logo
  45. created_at
  46. creator_id
  47. cover_url
  48. logo_url
  49. category
  50. )
  51. end
  52. 1 def true_methods
  53. %w[
  54. 2795 private_discussions_only?
  55. members_can_raise_motions
  56. members_can_edit_comments
  57. members_can_delete_comments
  58. discussion_private_default
  59. members_can_announce
  60. members_can_edit_discussions
  61. members_can_add_guests
  62. ]
  63. end
  64. 1 def empty_methods
  65. %w[
  66. 2795 member_ids
  67. identities
  68. hidden_poll_templates
  69. hidden_discussion_templates
  70. ]
  71. end
  72. 1 def discussion_privacy_options
  73. 'private_only'
  74. end
  75. 1 def false_methods
  76. %w(
  77. 2795 public_discussions_only?
  78. is_visible_to_parent_members
  79. members_can_add_members
  80. members_can_add_guests
  81. members_can_create_subgroups
  82. members_can_edit_discussions
  83. members_can_start_discussions
  84. admins_can_edit_user_content
  85. )
  86. end
  87. 1 def zero_methods
  88. %w[
  89. 2795 memberships_count
  90. polls_count
  91. closed_polls_count
  92. discussions_count
  93. public_discussions_count
  94. pending_memberships_count
  95. discussion_templates_count
  96. ]
  97. end
  98. 1 def none_methods
  99. 2795 {
  100. members: :user,
  101. self_and_subgroups: :group,
  102. accepted_members: :user,
  103. chatbots: :chatbot,
  104. tags: :tag,
  105. poll_templates: :poll_template,
  106. discussion_templates: :discussion_template,
  107. memberships: :membership,
  108. admins: :user,
  109. webhooks: :webhook,
  110. }
  111. end
  112. 1 def discussion_templates=(arg)
  113. nil
  114. end
  115. 1 def group_privacy
  116. 'private_only'
  117. end
  118. 1 def parent_or_self
  119. self
  120. end
  121. 1 def self_or_parent_logo_url(size)
  122. nil
  123. end
  124. 1 def self_or_parent_cover_url(size)
  125. nil
  126. end
  127. 1 def id_and_subgroup_ids
  128. []
  129. end
  130. 1 def poll_template_positions
  131. {
  132. 'question' => 0,
  133. 'check' => 1,
  134. 'advice' => 2,
  135. 'consent' => 3,
  136. 'consensus' => 4,
  137. 'gradients_of_agreement' => 5,
  138. 'poll' => 6,
  139. 'score' => 7,
  140. 'dot_vote' => 8,
  141. 'ranked_choice' => 9,
  142. 'meeting' => 10,
  143. 'count' => 11,
  144. }
  145. end
  146. 1 def discussion_template_positions
  147. {
  148. 'blank' => 0,
  149. 'open_discussion' => 1,
  150. 'updates_thread' => 2,
  151. }
  152. end
  153. 1 def subscription
  154. {
  155. max_members: nil,
  156. max_threads: nil,
  157. active: true,
  158. members_count: 0
  159. }
  160. end
  161. end

app/models/concerns/null/object.rb

88.24% lines covered

34 relevant lines. 30 lines covered and 4 lines missed.
    
  1. 1 module Null::Object
  2. 1 def apply_null_methods!
  3. 6100 apply_null_method :nil_methods, nil
  4. 6100 apply_null_method :false_methods, false
  5. 6100 apply_null_method :empty_methods, []
  6. 6100 apply_null_method :hash_methods, {}
  7. 6100 apply_null_method :true_methods, true
  8. 6100 apply_null_method :zero_methods, 0
  9. 6118 apply_null_method :none_methods, ->(model) { model.to_s.singularize.classify.constantize.none }
  10. end
  11. 1 def apply_null_method(name, value)
  12. 42700 send(name).each do |method, model|
  13. 380995 self.class.send :define_method, method, ->(*args) {
  14. 5518 value.respond_to?(:call) ? value.call(model) : value
  15. }
  16. end
  17. end
  18. 1 def blank?
  19. 438 true
  20. end
  21. 1 def present?
  22. 9 false
  23. end
  24. 1 def presence
  25. nil
  26. end
  27. 1 def marked_for_destruction?
  28. 90 false
  29. end
  30. 1 def nil_methods
  31. []
  32. end
  33. 1 def false_methods
  34. []
  35. end
  36. 1 def empty_methods
  37. []
  38. end
  39. 1 def hash_methods
  40. 2795 []
  41. end
  42. 1 def true_methods
  43. 3305 []
  44. end
  45. 1 def zero_methods
  46. 3305 []
  47. end
  48. 1 def none_methods
  49. []
  50. end
  51. end

app/models/concerns/null/user.rb

85.19% lines covered

27 relevant lines. 23 lines covered and 4 lines missed.
    
  1. 1 module Null::User
  2. # include HasAvatar
  3. 1 include Null::Object
  4. 1 def can?(*args)
  5. 3 ability.can?(*args)
  6. end
  7. 1 def ability
  8. 26 @ability ||= Ability::Base.new(self)
  9. end
  10. 1 def nil_methods
  11. 3305 [:id, :key, :username, :short_bio, :city, :region, :country, :selected_locale, :deactivated_at,
  12. :default_membership_volume, :unsubscribe_token, :location, :email_catch_up_day,
  13. :encrypted_password, :update_attribute, :last_seen_at, :legal_accepted_at, :api_key]
  14. end
  15. 1 def false_methods
  16. 3305 [:is_logged_in?, :is_member_of?, :is_admin_of?, :is_admin?, :is_admin, :api_key_changed?,
  17. :email_when_proposal_closing_soon, :has_password, :bot, :bot?,
  18. :email_when_mentioned, :email_on_participation, :email_verified, :email_verified?, :email_newsletter, :marked_for_destruction?]
  19. end
  20. 1 def empty_methods
  21. 3305 [:group_ids, :adminable_group_ids, :group_ids, :attachments, :guest_discussion_ids]
  22. end
  23. 1 def hash_methods
  24. 3305 [:experiences]
  25. end
  26. 1 def none_methods
  27. 3305 {
  28. notifications: :notification,
  29. login_tokens: :login_token,
  30. memberships: :membership,
  31. admin_memberships: :membership,
  32. participated_polls: :poll,
  33. group_polls: :poll,
  34. polls: :poll,
  35. stances: :stance,
  36. groups: :group,
  37. adminable_groups: :group
  38. }
  39. end
  40. 1 def avatar_initials_url(size = 256)
  41. params = {
  42. 409 name: "AU".split('').join('+'),
  43. background: AppConfig.theme[:brand_colors][:gold].gsub('#',''),
  44. color: '000000',
  45. rounded: true,
  46. format: :png,
  47. size: size
  48. }
  49. 2863 "https://ui-avatars.com/api/?#{params.to_a.map{|p| p.join('=')}.join('&')}"
  50. end
  51. 1 def default_format
  52. "html"
  53. end
  54. 1 def short_bio_format
  55. "html"
  56. end
  57. 1 def identities
  58. Identities::Base.none
  59. end
  60. 1 def is_admin?
  61. false
  62. end
  63. end

app/models/concerns/reactable.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. module Reactable
  2. def self.included(base)
  3. base.has_many :reactions, -> { joins(:user).where("users.deactivated_at": nil) }, dependent: :destroy, as: :reactable
  4. base.has_many :reactors, through: :reactions, source: :user
  5. end
  6. end

app/models/concerns/readable_unguessable_urls.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. module ReadableUnguessableUrls
  2. extend ActiveSupport::Concern
  3. KEY_LENGTH = 8
  4. included do |base|
  5. base.extend FriendlyId
  6. base.send :friendly_id, :key, use: [:finders]
  7. base.send :before_validation, :set_key
  8. base.send :validates, :key, presence: true
  9. end
  10. def set_key
  11. if self.key.blank?
  12. self.key = generate_unique_key
  13. end
  14. end
  15. private
  16. def generate_unique_key
  17. begin
  18. key = generate_key
  19. end while self.class.default_scoped.where(key: key).exists? or key.match(/^\d+$/)
  20. key
  21. end
  22. def generate_key
  23. (('a'..'z').to_a +
  24. ('A'..'Z').to_a +
  25. (0..9).to_a ).sample(KEY_LENGTH).join
  26. end
  27. end

app/models/concerns/routing.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. module Routing
  2. extend ActiveSupport::Concern
  3. include Rails.application.routes.url_helpers
  4. included do
  5. def default_url_options
  6. ActionMailer::Base.default_url_options
  7. end
  8. end
  9. end

app/models/concerns/searchable.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. module Searchable
  2. extend ActiveSupport::Concern
  3. include PgSearch::Model
  4. included do
  5. multisearchable
  6. end
  7. module ClassMethods
  8. def rebuild_pg_search_documents
  9. connection.execute pg_search_insert_statement
  10. end
  11. def pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
  12. raise "expected to be overwritten"
  13. end
  14. end
  15. end
  16. module PgSearch::Multisearchable
  17. def update_pg_search_document
  18. PgSearch::Document.where(searchable: self).delete_all
  19. ActiveRecord::Base.connection.execute(self.class.pg_search_insert_statement(id: self.id))
  20. end
  21. end

app/models/concerns/self_referencing.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module SelfReferencing
  2. extend ActiveSupport::Concern
  3. included do |base|
  4. define_method base.name.downcase, -> { self }
  5. define_method :"#{base.name.downcase}_id", -> { self.id }
  6. end
  7. end

app/models/concerns/translatable.rb

0.0% lines covered

22 relevant lines. 0 lines covered and 22 lines missed.
    
  1. module Translatable
  2. extend ActiveSupport::Concern
  3. included do
  4. has_many :translations, as: :translatable
  5. before_update :clear_translations, if: :translatable_fields_modified?
  6. end
  7. def translatable_fields_modified?
  8. return unless TranslationService.available?
  9. (self.saved_changes.keys.map(&:to_sym) & self.class.translatable_fields).any?
  10. end
  11. def clear_translations
  12. self.translations.delete_all
  13. end
  14. module ClassMethods
  15. def is_translatable(on: [], load_via: :find, id_field: :id, locale_field: :locale)
  16. define_singleton_method :translatable_fields, -> { Array on }
  17. define_singleton_method :get_instance, ->(id) { send load_via, id }
  18. define_method :id_field, -> { send id_field }
  19. define_method :locale_field, -> { send locale_field }
  20. end
  21. end
  22. end

app/models/concerns/uses_organisation_scope.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. module UsesOrganisationScope
  2. extend ActiveSupport::Concern
  3. included do
  4. scope :in_organisation, -> (group) { where(group_id: group.id_and_subgroup_ids) }
  5. end
  6. end

app/models/contact_message.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. class ContactMessage
  2. include ActiveModel::Model
  3. include ActiveModel::Validations
  4. alias :read_attribute_for_serialization :send
  5. attr_accessor :name, :email, :user_id, :subject, :message
  6. validates :email, presence: true, email: true
  7. # validates :message, presence: true, length: { maximum: Rails.application.secrets.max_message_length }
  8. end

app/models/demo.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Demo < ApplicationRecord
  2. belongs_to :author, class_name: 'User'
  3. belongs_to :group
  4. validates :name, presence: true
  5. def self.ransackable_attributes(auth_object = nil)
  6. ["author_id", "created_at", "demo_handle", "description", "group_id", "id", "name", "priority", "recorded_at", "updated_at"]
  7. end
  8. end

app/models/discussion.rb

94.24% lines covered

139 relevant lines. 131 lines covered and 8 lines missed.
    
  1. 1 class Discussion < ApplicationRecord
  2. 1 include CustomCounterCache::Model
  3. 1 include ReadableUnguessableUrls
  4. 1 include Translatable
  5. 1 include Reactable
  6. 1 include HasTimeframe
  7. 1 include HasEvents
  8. 1 include HasMentions
  9. 1 include MessageChannel
  10. 1 include SelfReferencing
  11. 1 include HasCreatedEvent
  12. 1 include HasRichText
  13. 1 include HasTags
  14. 1 include Discard::Model
  15. 1 include Searchable
  16. 1 def self.pg_search_insert_statement(id: nil, author_id: nil)
  17. 4971 content_str = "regexp_replace(CONCAT_WS(' ', discussions.title, discussions.description, users.name), E'<[^>]+>', '', 'gi')"
  18. 4971 <<~SQL.squish
  19. INSERT INTO pg_search_documents (
  20. searchable_type,
  21. searchable_id,
  22. group_id,
  23. discussion_id,
  24. author_id,
  25. authored_at,
  26. content,
  27. ts_content,
  28. created_at,
  29. updated_at)
  30. SELECT 'Discussion' AS searchable_type,
  31. discussions.id AS searchable_id,
  32. discussions.group_id as group_id,
  33. discussions.id AS discussion_id,
  34. discussions.author_id AS author_id,
  35. discussions.created_at AS authored_at,
  36. #{content_str} AS content,
  37. to_tsvector('simple', #{content_str}) as ts_content,
  38. now() AS created_at,
  39. now() AS updated_at
  40. FROM discussions
  41. LEFT JOIN users ON users.id = discussions.author_id
  42. WHERE discarded_at IS NULL
  43. #{id ? " AND discussions.id = #{id.to_i} LIMIT 1" : ""}
  44. #{author_id ? " AND discussions.author_id = #{author_id.to_i}" : ""}
  45. SQL
  46. end
  47. 1 scope :dangling, -> { joins('left join groups g on discussions.group_id = g.id').where('group_id is not null and g.id is null') }
  48. 1 scope :in_organisation, -> (group) { includes(:author).where(group_id: group.id_and_subgroup_ids) }
  49. 6 scope :last_activity_after, -> (time) { where('last_activity_at > ?', time) }
  50. 16 scope :order_by_latest_activity, -> { order(last_activity_at: :desc) }
  51. 5 scope :recent, -> { where('last_activity_at > ?', 6.weeks.ago) }
  52. 4583 scope :visible_to_public, -> { kept.where(private: false) }
  53. 1 scope :not_visible_to_public, -> { kept.where(private: true) }
  54. 4584 scope :is_open, -> { kept.where(closed_at: nil) }
  55. 4573 scope :is_closed, -> { kept.where("closed_at is not null") }
  56. 1 validates_presence_of :title, :group, :author
  57. 1 validates :title, length: { maximum: 150 }
  58. 1 validates :description, length: { maximum: Rails.application.secrets.max_message_length }
  59. 1 validate :privacy_is_permitted_by_group
  60. 1 is_mentionable on: :description
  61. 1 is_translatable on: [:title, :description], load_via: :find_by_key!, id_field: :key
  62. 1 is_rich_text on: :description
  63. 1 has_paper_trail only: [:title, :description, :description_format, :private, :group_id, :author_id, :tags, :closed_at, :closer_id]
  64. 1 belongs_to :group, class_name: 'Group'
  65. 1 belongs_to :author, class_name: 'User'
  66. 1 belongs_to :user, foreign_key: 'author_id'
  67. 1 belongs_to :closer, foreign_key: 'closer_id', class_name: "User"
  68. 1 has_many :polls, dependent: :destroy
  69. 1 has_many :active_polls, -> { where(closed_at: nil) }, class_name: "Poll"
  70. 1 has_many :comments, dependent: :destroy
  71. 1 has_many :commenters, -> { uniq }, through: :comments, source: :user
  72. 1 has_many :documents, as: :model, dependent: :destroy
  73. 1 has_many :poll_documents, through: :polls, source: :documents
  74. 1 has_many :comment_documents, through: :comments, source: :documents
  75. 2409 has_many :items, -> { includes(:user) }, class_name: 'Event', dependent: :destroy
  76. 1 has_many :discussion_readers, dependent: :destroy
  77. 11 has_many :readers,-> { merge DiscussionReader.active }, through: :discussion_readers, source: :user
  78. 1 has_many :guests, -> { merge DiscussionReader.guests }, through: :discussion_readers, source: :user
  79. 1 has_many :admin_guests, -> { merge DiscussionReader.admins }, through: :discussion_readers, source: :user
  80. 1 include DiscussionExportRelations
  81. 1 scope :search_for, -> (q) do
  82. kept.where("discussions.title ilike ?", "%#{q}%")
  83. end
  84. 1 delegate :name, to: :group, prefix: :group
  85. 1 delegate :name, to: :author, prefix: :author
  86. 1 delegate :users, to: :group, prefix: :group
  87. 1 delegate :full_name, to: :group, prefix: :group
  88. 1 delegate :email, to: :author, prefix: :author
  89. 1 delegate :name_and_email, to: :author, prefix: :author
  90. 1 delegate :locale, to: :author
  91. 1 after_create :set_last_activity_at_to_created_at
  92. 1 after_destroy :drop_sequence_id_sequence
  93. 502 define_counter_cache(:closed_polls_count) { |d| d.polls.closed.count }
  94. 11 define_counter_cache(:versions_count) { |d| d.versions.count }
  95. 1165 define_counter_cache(:seen_by_count) { |d| d.discussion_readers.where("last_read_at is not null").count }
  96. 1575 define_counter_cache(:members_count) { |d| d.discussion_readers.where("revoked_at is null").count }
  97. 502 define_counter_cache(:anonymous_polls_count) { |d| d.polls.where(anonymous: true).count }
  98. 1 update_counter_cache :group, :discussions_count
  99. 1 update_counter_cache :group, :public_discussions_count
  100. 1 update_counter_cache :group, :open_discussions_count
  101. 1 update_counter_cache :group, :closed_discussions_count
  102. 1 update_counter_cache :group, :closed_polls_count
  103. 1 def poll
  104. nil
  105. end
  106. 1 def group
  107. 42167 super || NullGroup.new
  108. end
  109. 1 def existing_member_ids
  110. reader_ids
  111. end
  112. 1 def user_id
  113. 1336 author_id
  114. end
  115. 1 def author
  116. 8856 super || AnonymousUser.new
  117. end
  118. 1 def members
  119. 4549 User.active.
  120. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id").
  121. joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
  122. where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
  123. (dr.id IS NOT NULL AND dr.guest = TRUE AND dr.revoked_at IS NULL)')
  124. end
  125. 1 def admins
  126. 86 User.active.
  127. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id").
  128. joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
  129. where('(m.admin = TRUE AND m.id IS NOT NULL AND m.revoked_at IS NULL) OR
  130. (dr.admin = TRUE AND dr.id IS NOT NULL AND dr.revoked_at IS NULL)')
  131. end
  132. 1 def guests
  133. 577 members.where('m.group_id is null')
  134. end
  135. 1 def add_guest!(user, inviter)
  136. 64 if dr = discussion_readers.find_by(user: user)
  137. 1 dr.update(guest: true, inviter: inviter)
  138. else
  139. 63 discussion_readers.create!(user: user, inviter: inviter, guest: true, volume: DiscussionReader.volumes[:normal])
  140. end
  141. end
  142. 1 def add_admin!(user, inviter)
  143. if dr = discussion_readers.find_by(user: user)
  144. dr.update(inviter: inviter, admin: true)
  145. else
  146. discussion_readers.create!(user: user, inviter: inviter, admin: true, volume: DiscussionReader.volumes[:normal])
  147. end
  148. end
  149. 1 def poll_id
  150. nil
  151. end
  152. 1 def created_event_kind
  153. 1617 :new_discussion
  154. end
  155. 1 def update_sequence_info!
  156. 1267 sequence_ids = discussion.items.order(:sequence_id).pluck(:sequence_id).compact
  157. 1267 discussion.ranges_string = RangeSet.serialize RangeSet.reduce RangeSet.ranges_from_list sequence_ids
  158. 1267 discussion.last_activity_at = discussion.items.unreadable.order(:sequence_id).last&.created_at || created_at
  159. 1267 update_columns(
  160. items_count: sequence_ids.count,
  161. ranges_string: discussion.ranges_string,
  162. last_activity_at: discussion.last_activity_at)
  163. end
  164. 1 def drop_sequence_id_sequence
  165. 10 SequenceService.drop_seq!('discussions_sequence_id', id)
  166. end
  167. 1 def public?
  168. 1554 !private
  169. end
  170. 1 def discussion
  171. 9760 self
  172. end
  173. 1 def body=(val)
  174. 10 self.description=(val)
  175. end
  176. 1 def body
  177. 562 self.description
  178. end
  179. 1 def body_format
  180. 553 self.description_format
  181. end
  182. 1 def body_format=(val)
  183. 5 self.description_format=(val)
  184. end
  185. 1 def ranges
  186. 1042 RangeSet.parse(self.ranges_string)
  187. end
  188. 1 def first_sequence_id
  189. 5 Array(ranges.first).first.to_i
  190. end
  191. 1 def last_sequence_id
  192. 6 Array(ranges.last).last.to_i
  193. end
  194. # this is insted of a big slow migration
  195. 1 def ranges_string
  196. 2309 update_sequence_info! if self[:ranges_string].nil?
  197. 2309 self[:ranges_string]
  198. end
  199. 1 def is_new_version?
  200. (['title', 'description', 'private'] & self.changes.keys).any?
  201. end
  202. 1 private
  203. 1 def set_last_activity_at_to_created_at
  204. 1018 update_column(:last_activity_at, created_at)
  205. end
  206. 1 def sequence_id_or_0(item)
  207. item.try(:sequence_id) || 0
  208. end
  209. 1 def privacy_is_permitted_by_group
  210. 1553 if self.public? and group.private_discussions_only?
  211. 1 errors.add(:private, "must be private")
  212. end
  213. 1553 if self.private? and group.public_discussions_only?
  214. errors.add(:private, "must be public")
  215. end
  216. end
  217. end

app/models/discussion_reader.rb

93.42% lines covered

76 relevant lines. 71 lines covered and 5 lines missed.
    
  1. 1 class DiscussionReader < ApplicationRecord
  2. 1 include CustomCounterCache::Model
  3. 1 include HasVolume
  4. 1 extend HasTokens
  5. 1 initialized_with_token :token
  6. 1 belongs_to :user
  7. 1 belongs_to :discussion
  8. 1 belongs_to :inviter, class_name: 'User'
  9. 1 delegate :message_channel, to: :user
  10. 1 scope :dangling, -> { joins('left join discussions on discussions.id = discussion_id left join users on users.id = user_id').where('discussions.id is null or users.id is null') }
  11. 1381 scope :active, -> { where("discussion_readers.revoked_at IS NULL") }
  12. 667 scope :guests, -> { active.where('discussion_readers.guest': true) }
  13. 1 scope :admins, -> { active.where('discussion_readers.admin': true) }
  14. 626 scope :redeemable, -> { guests.where('discussion_readers.accepted_at IS NULL') }
  15. 3 scope :redeemable_by, -> (user_id) { redeemable.joins(:user).where("user_id = ? OR users.email_verified = false", user_id) }
  16. 1 update_counter_cache :discussion, :seen_by_count
  17. 1 update_counter_cache :discussion, :members_count
  18. 1 def self.for(user:, discussion:)
  19. 1342 if user&.is_logged_in?
  20. 1342 find_or_initialize_by(user_id: user.id, discussion_id: discussion.id) do |dr|
  21. 762 m = user.memberships.find_by(group_id: discussion.group_id)
  22. 762 dr.volume = (m && m.volume) || 'normal'
  23. end
  24. else
  25. new(discussion: discussion)
  26. end
  27. end
  28. 1 def self.for_model(model, actor = nil)
  29. 790 self.for(user: actor || model.author, discussion: model.discussion)
  30. end
  31. 1 def update_reader(ranges: nil, volume: nil, participate: false, dismiss: false)
  32. 786 viewed!(ranges, persist: false) if ranges
  33. 786 set_volume!(volume, persist: false) if volume && (volume != :loud || user.email_on_participation?)
  34. 786 dismiss!(persist: false) if dismiss
  35. 786 save! if changed?
  36. 786 self
  37. end
  38. 1 def viewed!(ranges = [], persist: true)
  39. 427 mark_as_read(ranges) unless has_read?(ranges)
  40. 427 assign_attributes(last_read_at: Time.now)
  41. 427 save if persist
  42. end
  43. 1 def has_read?(ranges = [])
  44. 437 RangeSet.includes?(read_ranges, ranges)
  45. end
  46. 1 def mark_as_read(ranges)
  47. 427 ranges = RangeSet.to_ranges(ranges)
  48. 427 return if ranges.empty?
  49. 427 self.read_ranges = read_ranges.concat(ranges)
  50. end
  51. 1 def dismiss!(persist: true)
  52. 3 self.dismissed_at = Time.zone.now
  53. 3 save if persist
  54. end
  55. 1 def recall!(persist: true)
  56. 1 self.dismissed_at = nil
  57. 1 save if persist
  58. end
  59. 1 def computed_volume
  60. 8 if persisted?
  61. 7 volume || membership&.volume || 'normal'
  62. else
  63. 1 membership.volume
  64. end
  65. end
  66. 1 def discussion_reader_volume
  67. 154 self[:volume]
  68. end
  69. 1 def discussion_reader_user_id
  70. 154 self.user_id
  71. end
  72. 1 def read_ranges
  73. 1075 RangeSet.parse(self.read_ranges_string)
  74. end
  75. 1 def read_ranges=(ranges)
  76. 427 ranges = RangeSet.reduce(ranges)
  77. 427 self.read_ranges_string = RangeSet.serialize(ranges)
  78. end
  79. 1 def first_unread_sequence_id
  80. Array(unread_ranges.first).first.to_i
  81. end
  82. # maybe yagni, because the client should do this locally
  83. 1 def unread_ranges
  84. RangeSet.subtract_ranges(discussion.ranges, read_ranges)
  85. end
  86. 1 def read_ranges_string
  87. 1081 self[:read_ranges_string] ||= begin
  88. 511 if last_read_sequence_id == 0
  89. 511 ""
  90. else
  91. "#{[discussion.first_sequence_id, 1].max}-#{last_read_sequence_id}"
  92. end
  93. end
  94. end
  95. 1 def read_items_count
  96. 8 RangeSet.length(read_ranges)
  97. end
  98. 1 def unread_items_count
  99. RangeSet.length(unread_ranges)
  100. end
  101. 1 private
  102. 1 def membership
  103. 1 @membership ||= discussion.group.membership_for(user)
  104. end
  105. end

app/models/discussion_template.rb

53.57% lines covered

28 relevant lines. 15 lines covered and 13 lines missed.
    
  1. 1 class DiscussionTemplate < ApplicationRecord
  2. 1 include Discard::Model
  3. 1 include HasRichText
  4. 1 include CustomCounterCache::Model
  5. 1 is_rich_text on: :description
  6. 1 belongs_to :author, class_name: "User"
  7. 1 belongs_to :group, class_name: "Group"
  8. 1 update_counter_cache :group, :discussion_templates_count
  9. 1 validates :description, length: { maximum: Rails.application.secrets.max_message_length }
  10. 1 validates :process_name, presence: true
  11. # validates :process_subtitle, presence: true
  12. 1 has_paper_trail only: [
  13. :public,
  14. :title,
  15. :process_name,
  16. :process_subtitle,
  17. :process_introduction,
  18. :process_introduction_format,
  19. :description,
  20. :description_format,
  21. :group_id,
  22. :tags,
  23. :discarded_at
  24. ]
  25. 1 def members
  26. User.none
  27. end
  28. 1 def dump_i18n
  29. out = {}
  30. [
  31. :title,
  32. :title_placeholder,
  33. :process_name,
  34. :process_subtitle,
  35. :process_introduction,
  36. :description,
  37. ].map(&:to_s).each do |key|
  38. value = self[key]
  39. next unless value
  40. value.strip! if value.respond_to? :strip!
  41. out[key] = value
  42. end
  43. tags.each do |tag|
  44. out[tag.underscore.gsub(" ", "_")] = tag
  45. end
  46. {process_name.strip.underscore.gsub(" ", "_") => out}
  47. end
  48. 1 def poll_templates
  49. PollTemplate.where(id: poll_template_ids)
  50. end
  51. 1 def poll_template_ids
  52. self.poll_template_keys_or_ids.filter do |key_or_id|
  53. key_or_id.is_a? Integer
  54. end
  55. end
  56. end

app/models/document.rb

88.57% lines covered

35 relevant lines. 31 lines covered and 4 lines missed.
    
  1. 1 class Document < ApplicationRecord
  2. 1 belongs_to :model, polymorphic: true, required: false
  3. 1 belongs_to :author, class_name: 'User', required: true
  4. 1 validates :title, presence: true
  5. 1 validates :doctype, presence: true
  6. 1 validates :color, presence: true
  7. 1 before_validation :set_metadata
  8. 1 before_save :set_group_id
  9. 1 has_one_attached :file
  10. 1 scope :search_for, ->(query) {
  11. 5 if query.present?
  12. where("title ilike :q", q: "%#{query}%")
  13. else
  14. 5 all
  15. end
  16. }
  17. 1 def download_url
  18. 18 return nil unless file.attached?
  19. Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true)
  20. end
  21. 1 def reset_metadata!
  22. update(doctype: metadata['name'], icon: metadata['icon'], color: metadata['color'])
  23. end
  24. 1 [:group, :discussion, :poll].map do |model_type|
  25. 21 define_method model_type, -> { self.model.send(model_type) if self.model.respond_to?(model_type) }
  26. end
  27. 1 def is_an_image?
  28. metadata['icon'] == 'image'
  29. end
  30. 1 def url
  31. 513 return file.url if file.attached?
  32. 513 return nil unless self[:url]
  33. 513 self[:url].to_s.starts_with?("http") ? self[:url] : "#{lmo_asset_host}#{self[:url]}"
  34. end
  35. 1 private
  36. 1 def set_group_id
  37. 34 self.group_id = model.group_id if model && model.respond_to?(:group_id)
  38. end
  39. 1 def set_metadata
  40. 34 self.doctype ||= metadata['name']
  41. 34 self.icon ||= metadata['icon']
  42. 34 self.color ||= metadata['color']
  43. end
  44. 1 def metadata
  45. 612 @metadata ||= Hash(AppConfig.doctypes.detect { |type| /#{type['regex']}/.match(file.content_type || url) })
  46. end
  47. end

app/models/event.rb

0.0% lines covered

178 relevant lines. 0 lines covered and 178 lines missed.
    
  1. class Event < ApplicationRecord
  2. include ActionView::Helpers::SanitizeHelper
  3. include CustomCounterCache::Model
  4. include HasTimeframe
  5. extend HasCustomFields
  6. has_many :notifications, dependent: :destroy
  7. belongs_to :eventable, polymorphic: true
  8. belongs_to :discussion, required: false
  9. belongs_to :user, required: false
  10. belongs_to :parent, class_name: "Event", required: false
  11. has_many :children, (-> { where("discussion_id is not null") }), class_name: "Event", foreign_key: :parent_id
  12. set_custom_fields :pinned_title, :recipient_user_ids, :recipient_chatbot_ids, :recipient_message, :recipient_audience, :stance_ids
  13. before_create :set_parent_and_depth, if: :discussion_id
  14. before_create :set_sequences, if: :discussion_id
  15. after_rollback :reset_sequences, if: :discussion_id
  16. before_destroy :reset_sequences, if: :discussion_id
  17. after_create :update_sequence_info!, if: :discussion_id
  18. after_destroy :update_sequence_info!, if: :discussion_id
  19. define_counter_cache(:child_count) { |e| e.children.count }
  20. define_counter_cache(:descendant_count) { |e|
  21. if e.kind == "new_discussion"
  22. Event.where(discussion_id: e.eventable_id).count
  23. elsif e.position_key && e.discussion_id
  24. Event.where(discussion_id: e.discussion_id).
  25. where("id != ?", e.id).
  26. where('position_key like ?', e.position_key+"%").count
  27. else
  28. 0
  29. end
  30. }
  31. update_counter_cache :parent, :child_count
  32. update_counter_cache :parent, :descendant_count
  33. validates :kind, presence: true
  34. validates :eventable, presence: true
  35. scope :dangling, -> { joins('left join discussions d on events.discussion_id = d.id').where('d.id is null and discussion_id is not null') }
  36. scope :unreadable, -> { where.not(kind: 'discussion_closed') }
  37. scope :invitations_in_period, ->(since, till) {
  38. where(kind: :announcement_created, eventable_type: 'Group').within(since.beginning_of_hour, till.beginning_of_hour)
  39. }
  40. delegate :group, to: :eventable, allow_nil: true
  41. delegate :poll, to: :eventable, allow_nil: true
  42. delegate :groups, to: :eventable, allow_nil: true
  43. delegate :update_sequence_info!, to: :discussion, allow_nil: true
  44. def self.sti_find(id)
  45. e = self.find(id)
  46. e.kind_class.find(id)
  47. end
  48. def kind_class
  49. ("Events::"+kind.classify).constantize
  50. end
  51. def self.publish!(eventable, **args)
  52. event = build(eventable, **args)
  53. event.save!
  54. PublishEventWorker.perform_async(event.id)
  55. event
  56. end
  57. def self.build(eventable, **args)
  58. new({
  59. kind: name.demodulize.underscore,
  60. eventable: eventable,
  61. eventable_version_id: ((eventable.respond_to?(:versions) && eventable.versions.last&.id) || nil)
  62. }.merge(args))
  63. end
  64. def user
  65. super || AnonymousUser.new
  66. end
  67. def real_user
  68. user
  69. end
  70. def actor
  71. user
  72. end
  73. def actor_id
  74. user_id
  75. end
  76. def message_channel
  77. eventable.group.message_channel
  78. end
  79. # this is called after create, and calls methods defined by the event concerns
  80. # included per event type
  81. def trigger!
  82. EventBus.broadcast("#{kind}_event", self)
  83. end
  84. def active_model_serializer
  85. "Events::#{eventable.class.to_s.split('::').last}Serializer".constantize
  86. rescue NameError
  87. EventSerializer
  88. end
  89. def set_parent_and_depth
  90. self.parent = max_depth_adjusted_parent
  91. self.depth = parent ? parent.depth + 1 : 0
  92. end
  93. def set_parent_and_depth!
  94. set_parent_and_depth
  95. update_columns(parent_id: parent_id, depth: depth)
  96. end
  97. def set_sequences
  98. self.sequence_id = next_sequence_id!
  99. self.position = next_position!
  100. # self.position_key = self_and_parents.reverse.map(&:position).map{|p| Event.zero_fill(p) }.join('-')
  101. self.position_key = [parent&.position_key, Event.zero_fill(position)].compact.join('-')
  102. end
  103. def set_sequence_id!
  104. update_attribute(:sequence_id, next_sequence_id!)
  105. end
  106. def reset_sequences
  107. SequenceService.drop_seq!('discussions_sequence_id', discussion_id)
  108. EventService.reset_child_positions(parent.id, parent.position_key) if parent_id && parent
  109. end
  110. def next_sequence_id!
  111. unless SequenceService.seq_present?('discussions_sequence_id', discussion_id)
  112. val = Event.
  113. where(discussion_id: discussion_id).
  114. where("sequence_id is not null").
  115. order(sequence_id: :desc).
  116. limit(1).pluck(:sequence_id).last || 0
  117. SequenceService.create_seq!('discussions_sequence_id', discussion_id, val)
  118. end
  119. SequenceService.next_seq!('discussions_sequence_id', discussion_id)
  120. end
  121. def next_position!
  122. return 0 unless (discussion_id and parent_id)
  123. unless SequenceService.seq_present?('events_position', parent_id)
  124. val = Event.where(parent_id: parent_id,
  125. discussion_id: discussion_id).
  126. order(position: :desc).
  127. limit(1).pluck(:position).last || 0
  128. SequenceService.create_seq!('events_position', parent_id, val)
  129. end
  130. SequenceService.next_seq!('events_position', parent_id)
  131. end
  132. def self.zero_fill(num)
  133. "0" * (5 - num.to_s.length) + num.to_s
  134. end
  135. def find_parent_event
  136. case kind
  137. when 'discussion_closed' then eventable.created_event
  138. when 'discussion_forked' then eventable.created_event
  139. when 'discussion_moved' then discussion.created_event
  140. when 'discussion_edited' then (eventable || discussion)&.created_event
  141. when 'discussion_reopened' then eventable.created_event
  142. when 'outcome_created' then eventable.parent_event
  143. when 'new_comment' then eventable.parent_event
  144. when 'poll_closed_by_user' then eventable.created_event
  145. when 'poll_closing_soon' then eventable.created_event
  146. when 'poll_created' then eventable.parent_event
  147. when 'poll_edited' then eventable.created_event
  148. when 'poll_expired' then eventable.created_event
  149. when 'poll_option_added' then eventable.created_event
  150. when 'poll_reopened' then eventable.created_event
  151. when 'stance_created' then eventable.parent_event
  152. when 'stance_updated' then eventable.parent_event
  153. else
  154. nil
  155. end
  156. end
  157. def self_and_parents
  158. [self, (parent && parent.discussion_id && parent.self_and_parents)].flatten.compact
  159. end
  160. def max_depth_adjusted_parent
  161. original_parent = find_parent_event
  162. return nil unless original_parent
  163. if discussion && discussion.max_depth == original_parent.depth
  164. original_parent.parent
  165. else
  166. original_parent
  167. end
  168. end
  169. def email_recipients
  170. Queries::UsersByVolumeQuery.email_notifications(eventable).where(id: all_recipient_user_ids)
  171. end
  172. def notification_recipients
  173. Queries::UsersByVolumeQuery.app_notifications(eventable).where(id: all_recipient_user_ids).where.not(id: user.id || 0)
  174. end
  175. def all_recipients
  176. User.active.where(id: all_recipient_user_ids)
  177. end
  178. def all_recipient_user_ids
  179. (recipient_user_ids || []).uniq.compact #.without(actor_id)
  180. end
  181. end

app/models/events/announcement_resend.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class Events::AnnouncementResend < Event
  2. include Events::Notify::ByEmail
  3. def self.publish!(event)
  4. super event.eventable,
  5. user: event.user,
  6. custom_fields: {
  7. membership_ids: Membership.pending.where(id: event.custom_fields['membership_ids']).pluck(:id),
  8. kind: event.custom_fields['kind']
  9. }
  10. end
  11. def email_method
  12. 'group_announced'
  13. end
  14. def email_recipients
  15. # return User.none if eventable.is_a?(Poll) && !eventable.active? # do we want this?
  16. User.active.where(id: Membership.where(id: custom_fields['membership_ids']).pluck(:user_id))
  17. end
  18. end

app/models/events/comment_edited.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Events::CommentEdited < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::Mentions
  4. 1 def self.publish!(comment, actor)
  5. 5 super(comment, user: actor)
  6. end
  7. end

app/models/events/comment_replied_to.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Events::CommentRepliedTo < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(comment)
  5. 5 super comment, user: comment.author
  6. end
  7. 1 private
  8. 1 def email_recipients
  9. 5 notification_recipients.where(email_when_mentioned: true)
  10. end
  11. 1 def notification_recipients
  12. 10 eventable.members.where('users.id': eventable.parent.author_id).where.not('users.id': eventable.author_id)
  13. end
  14. end

app/models/events/discussion_announced.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class Events::DiscussionAnnounced < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 include Events::Notify::Chatbots
  5. 1 def self.publish!(
  6. discussion:,
  7. actor:,
  8. recipient_user_ids:,
  9. recipient_chatbot_ids:,
  10. recipient_audience: nil,
  11. recipient_message: nil)
  12. 9 super(discussion,
  13. user: actor,
  14. recipient_user_ids: recipient_user_ids,
  15. recipient_chatbot_ids: recipient_chatbot_ids,
  16. recipient_audience: recipient_audience.presence,
  17. recipient_message: recipient_message.presence)
  18. end
  19. end

app/models/events/discussion_closed.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class Events::DiscussionClosed < Event
  2. include Events::LiveUpdate
  3. def self.publish!(discussion, actor)
  4. super discussion,
  5. user: actor,
  6. discussion: discussion,
  7. created_at: discussion.closed_at
  8. end
  9. end

app/models/events/discussion_description_edited.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class Events::DiscussionDescriptionEdited < Event
  2. def self.publish!(discussion, editor)
  3. super discussion, user: editor
  4. end
  5. end

app/models/events/discussion_edited.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class Events::DiscussionEdited < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::InApp
  4. 1 include Events::Notify::ByEmail
  5. 1 include Events::Notify::Mentions
  6. 1 include Events::Notify::Chatbots
  7. 1 def self.publish!(
  8. discussion:,
  9. actor:,
  10. recipient_user_ids: [],
  11. recipient_chatbot_ids: [],
  12. recipient_audience: nil,
  13. recipient_message: nil)
  14. 9 super(discussion,
  15. user: actor,
  16. 9 discussion_id: (recipient_message && discussion.id) || nil,
  17. recipient_user_ids: recipient_user_ids,
  18. recipient_chatbot_ids: recipient_chatbot_ids,
  19. recipient_audience: recipient_audience,
  20. recipient_message: recipient_message)
  21. end
  22. 1 def discussion
  23. 18 eventable
  24. end
  25. end

app/models/events/discussion_forked.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class Events::DiscussionForked < Event
  2. def self.publish!(discussion, source)
  3. super discussion,
  4. discussion: source,
  5. user: discussion.author,
  6. sequence_id: discussion.forked_items.minimum(:sequence_id)+1,
  7. created_at: discussion.created_at,
  8. custom_fields: { item_ids: discussion.forked_event_ids }
  9. end
  10. end

app/models/events/discussion_moved.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Events::DiscussionMoved < Event
  2. 1 include Events::LiveUpdate
  3. 1 def self.publish!(discussion, actor, source_group)
  4. 7 super discussion,
  5. discussion: discussion,
  6. custom_fields: { source_group_id: source_group.id },
  7. user: actor,
  8. created_at: Time.now
  9. end
  10. end

app/models/events/discussion_reopened.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Events::DiscussionReopened < Event
  2. include Events::LiveUpdate
  3. def self.publish!(discussion, actor)
  4. super discussion,
  5. user: actor,
  6. discussion: discussion
  7. end
  8. end

app/models/events/discussion_title_edited.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class Events::DiscussionTitleEdited < Event
  2. def self.publish!(discussion, editor)
  3. super discussion, user: editor, created_at: Time.now
  4. end
  5. end

app/models/events/group_identity_created.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. class Events::GroupIdentityCreated < Event
  2. def self.publish!(group_identity, actor)
  3. super group_identity,
  4. user: actor,
  5. announcement: group_identity.make_announcement
  6. end
  7. def identities
  8. return Identities::Base.none unless announcement
  9. super.where(id: eventable.identity_id)
  10. end
  11. end

app/models/events/invitation_accepted.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Events::InvitationAccepted < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::LiveUpdate
  4. 1 def self.publish!(membership)
  5. 11 super membership, user: membership.user
  6. end
  7. 1 private
  8. 1 def notification_recipients
  9. 11 User.where(id: eventable.inviter_id)
  10. end
  11. 1 def notification_actor
  12. 10 eventable&.user
  13. end
  14. 1 def notification_url
  15. 5 polymorphic_url(eventable.group)
  16. end
  17. end

app/models/events/membership_created.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Events::MembershipCreated < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(
  5. group:,
  6. actor:,
  7. recipient_user_ids:,
  8. recipient_message: nil)
  9. 13 super group,
  10. user: actor,
  11. recipient_message: recipient_message,
  12. recipient_user_ids: recipient_user_ids
  13. end
  14. end

app/models/events/membership_request_approved.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Events::MembershipRequestApproved < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(membership, approver)
  5. 1 super membership, user: approver
  6. end
  7. 1 private
  8. 1 def email_users!
  9. 1 email_recipients.active.uniq.pluck(:id).each do |recipient_id|
  10. 1 EventMailer.event(recipient_id, self.id).deliver_later
  11. end
  12. end
  13. 1 def notification_recipients
  14. 2 User.where(id: eventable&.user_id)
  15. end
  16. 1 alias :email_recipients :notification_recipients
  17. end

app/models/events/membership_requested.rb

0.0% lines covered

22 relevant lines. 0 lines covered and 22 lines missed.
    
  1. class Events::MembershipRequested < Event
  2. include Events::Notify::InApp
  3. include Events::Notify::ByEmail
  4. def self.publish!(membership_request)
  5. super membership_request, user: membership_request.requestor
  6. end
  7. private
  8. def notification_recipients
  9. eventable.admins.active
  10. end
  11. def email_recipients
  12. Queries::UsersByVolumeQuery.
  13. email_notifications(eventable.group).
  14. where(id: eventable.admins.active.pluck(:id))
  15. end
  16. def notification_actor
  17. eventable.requestor
  18. end
  19. def notification_translation_values
  20. { name: eventable.requestor&.name || eventable.name, title: eventable.group.full_name }
  21. end
  22. end

app/models/events/membership_resent.rb

78.57% lines covered

14 relevant lines. 11 lines covered and 3 lines missed.
    
  1. 1 class Events::MembershipResent < Event
  2. 1 include Events::Notify::ByEmail
  3. 1 def self.publish!(membership, actor)
  4. 1 super membership.group,
  5. user: actor,
  6. custom_fields: { membership_id: membership.id }
  7. end
  8. 1 private
  9. 1 def email_method
  10. :"#{eventable_key}_announced"
  11. end
  12. 1 def email_recipients
  13. 1 User.where(id: membership.user_id)
  14. end
  15. 1 def eventable_key
  16. return :group if eventable.is_a?(Group)
  17. eventable.class.to_s.downcase
  18. end
  19. 1 def membership
  20. 1 Membership.find(custom_fields['membership_id'])
  21. end
  22. end

app/models/events/new_comment.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Events::NewComment < Event
  2. 1 include Events::Notify::ByEmail
  3. 1 include Events::Notify::Mentions
  4. 1 include Events::Notify::Chatbots
  5. 1 include Events::LiveUpdate
  6. 1 def self.publish!(comment)
  7. 183 if comment.parent.present?
  8. 183 GenericWorker.perform_async('NotificationService', 'mark_as_read', comment.parent_type, comment.parent_id, comment.author_id)
  9. end
  10. 183 super comment,
  11. user: comment.author,
  12. discussion: comment.discussion,
  13. pinned: comment.should_pin
  14. end
  15. 1 private
  16. 1 def email_recipients
  17. 184 Queries::UsersByVolumeQuery.loud(eventable.discussion)
  18. .where.not(id: eventable.author)
  19. .where.not(id: eventable.mentioned_users)
  20. .where.not(id: eventable.parent_author).distinct
  21. end
  22. end

app/models/events/new_coordinator.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class Events::NewCoordinator < Event
  2. 1 include Events::Notify::InApp
  3. 1 def self.publish!(membership, actor)
  4. 2 super membership, user: actor, created_at: Time.now
  5. end
  6. 1 private
  7. 1 def notification_recipients
  8. 2 User.where(id: eventable.user_id)
  9. end
  10. end

app/models/events/new_discussion.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Events::NewDiscussion < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::InApp
  4. 1 include Events::Notify::ByEmail
  5. 1 include Events::Notify::Mentions
  6. 1 include Events::Notify::Chatbots
  7. 1 def self.publish!(
  8. discussion:,
  9. recipient_user_ids: [],
  10. recipient_chatbot_ids: [],
  11. recipient_audience: nil)
  12. 364 super(discussion,
  13. user: discussion.author,
  14. recipient_user_ids: recipient_user_ids,
  15. recipient_chatbot_ids: recipient_chatbot_ids,
  16. recipient_audience: recipient_audience.presence)
  17. end
  18. 1 def discussion
  19. 728 eventable
  20. end
  21. end

app/models/events/outcome_announced.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Events::OutcomeAnnounced < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(outcome, actor, user_ids, audience = nil)
  5. 15 super outcome,
  6. user: actor,
  7. recipient_user_ids: user_ids.uniq.compact,
  8. recipient_audience: audience.presence
  9. end
  10. end

app/models/events/outcome_created.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Events::OutcomeCreated < Event
  2. 1 include Events::Notify::Mentions
  3. 1 include Events::Notify::InApp
  4. 1 include Events::Notify::ByEmail
  5. 1 include Events::Notify::Chatbots
  6. 1 include Events::LiveUpdate
  7. 1 def self.publish!(
  8. outcome:,
  9. recipient_user_ids: [],
  10. recipient_chatbot_ids: [],
  11. recipient_audience: nil)
  12. 24 super(outcome,
  13. user: outcome.author,
  14. discussion: outcome.poll.discussion,
  15. recipient_user_ids: recipient_user_ids,
  16. recipient_chatbot_ids: recipient_chatbot_ids,
  17. recipient_audience: recipient_audience)
  18. end
  19. end

app/models/events/outcome_review_due.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class Events::OutcomeReviewDue < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 include Events::Notify::Chatbots
  5. 1 def self.publish!(outcome)
  6. 8 super outcome,
  7. user: outcome.author
  8. end
  9. 1 private
  10. 1 def email_recipients
  11. 8 Queries::UsersByVolumeQuery.email_notifications(poll)
  12. .where('users.id': raw_recipients.pluck(:id))
  13. end
  14. 1 def notification_recipients
  15. 8 Queries::UsersByVolumeQuery.app_notifications(poll)
  16. .where('users.id': raw_recipients.pluck(:id))
  17. end
  18. 1 def raw_recipients
  19. 16 User.where(id: eventable.author_id)
  20. end
  21. end

app/models/events/outcome_updated.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Events::OutcomeUpdated < Event
  2. 1 include Events::Notify::Mentions
  3. 1 include Events::Notify::InApp
  4. 1 include Events::Notify::ByEmail
  5. 1 include Events::Notify::Chatbots
  6. 1 include Events::LiveUpdate
  7. 1 def self.publish!(outcome:,
  8. actor:,
  9. recipient_user_ids: [],
  10. recipient_chatbot_ids: [],
  11. recipient_audience: nil)
  12. 1 super(outcome,
  13. user: actor,
  14. recipient_user_ids: recipient_user_ids,
  15. recipient_chatbot_ids: recipient_chatbot_ids,
  16. recipient_audience: recipient_audience)
  17. end
  18. end

app/models/events/poll_announced.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 class Events::PollAnnounced < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 include Events::Notify::Chatbots
  5. 1 def self.publish!(
  6. poll: ,
  7. actor: ,
  8. stances: ,
  9. recipient_user_ids: [],
  10. recipient_chatbot_ids: [],
  11. recipient_audience: nil,
  12. recipient_message: nil)
  13. 4 super poll,
  14. user: actor,
  15. stance_ids: stances.map(&:id),
  16. recipient_user_ids: recipient_user_ids,
  17. recipient_chatbot_ids: recipient_chatbot_ids,
  18. recipient_audience: recipient_audience.presence,
  19. recipient_message: recipient_message.presence
  20. end
  21. 1 private
  22. 1 def stances
  23. Stance.where(id: self.stance_ids)
  24. end
  25. 1 def email_recipients
  26. 4 notification_recipients.where(id: Queries::UsersByVolumeQuery.normal_or_loud(eventable))
  27. end
  28. 1 def notification_recipients
  29. 8 User.active.distinct.joins(:stances).where('stances.id IN (?)', self.stance_ids)
  30. end
  31. end

app/models/events/poll_closed_by_user.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Events::PollClosedByUser < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::Chatbots
  4. 1 def self.publish!(poll, actor)
  5. 6 super poll,
  6. user: actor,
  7. discussion: poll.discussion,
  8. created_at: poll.closed_at
  9. end
  10. end

app/models/events/poll_closing_soon.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 class Events::PollClosingSoon < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::Author
  4. 1 include Events::Notify::ByEmail
  5. 1 include Events::Notify::Chatbots
  6. 1 def self.publish!(poll)
  7. 32 super poll,
  8. user: poll.author,
  9. created_at: Time.now
  10. end
  11. 1 def notify_author?
  12. 33 eventable.notify_on_closing_soon == 'author'
  13. end
  14. 1 private
  15. 1 def email_recipients
  16. 35 Queries::UsersByVolumeQuery.email_notifications(poll)
  17. .where('users.id': raw_recipients.pluck(:id))
  18. end
  19. 1 def notification_recipients
  20. 35 Queries::UsersByVolumeQuery.app_notifications(poll)
  21. .where('users.id': raw_recipients.pluck(:id))
  22. end
  23. 1 def raw_recipients
  24. # work around for anonymous
  25. 70 case poll.notify_on_closing_soon
  26. # when 'author'
  27. # User.where(id: poll.author_id)
  28. when 'undecided_voters'
  29. 4 poll.unmasked_undecided_voters
  30. when 'voters'
  31. 46 poll.unmasked_voters
  32. else
  33. 20 User.none
  34. end
  35. end
  36. 1 def notification_translation_values
  37. 309 super.merge(poll_type: I18n.t(:"poll_types.#{eventable.poll_type}"))
  38. end
  39. end

app/models/events/poll_created.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Events::PollCreated < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::Mentions
  4. 1 include Events::Notify::Chatbots
  5. 1 include Events::Notify::ByEmail
  6. 1 include Events::Notify::InApp
  7. 1 def self.publish!(poll, actor, recipient_user_ids: [])
  8. 168 super poll,
  9. user: actor,
  10. discussion: poll.discussion,
  11. pinned: true,
  12. recipient_user_ids: recipient_user_ids
  13. end
  14. end

app/models/events/poll_edited.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Events::PollEdited < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 include Events::Notify::Mentions
  5. 1 include Events::Notify::Chatbots
  6. 1 def self.publish!(
  7. poll:,
  8. actor:,
  9. recipient_user_ids: [],
  10. recipient_chatbot_ids: [],
  11. recipient_message: nil,
  12. recipient_audience: nil)
  13. 7 super(poll,
  14. 7 discussion_id: (recipient_message && poll.discussion_id) || nil,
  15. user: actor,
  16. recipient_user_ids: recipient_user_ids,
  17. recipient_chatbot_ids: recipient_chatbot_ids,
  18. recipient_audience: recipient_audience.presence,
  19. recipient_message: recipient_message.presence)
  20. end
  21. end

app/models/events/poll_expired.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class Events::PollExpired < Event
  2. 1 include Events::Notify::Author
  3. 1 include Events::Notify::Chatbots
  4. 1 include Events::Notify::InApp
  5. 1 def self.publish!(poll)
  6. 18 super poll,
  7. user: poll.author,
  8. discussion: nil,
  9. created_at: poll.closed_at
  10. end
  11. # email the author and create an in-app notification
  12. 1 def email_author!
  13. 18 super
  14. 18 notification_for(author).save
  15. end
  16. 1 def notify_author?
  17. 18 return false unless eventable.present? && eventable.poll.present?
  18. 18 Queries::UsersByVolumeQuery.email_notifications(eventable).exists?(eventable.poll.author_id)
  19. end
  20. end

app/models/events/poll_option_added.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class Events::PollOptionAdded < Event
  2. include Events::Notify::Author
  3. include Events::Notify::InApp
  4. def self.publish!(poll, actor, poll_option_names = [])
  5. return unless Array(poll_option_names).any?
  6. super poll,
  7. user: (actor unless poll.anonymous?),
  8. custom_fields: { poll_option_names: poll_option_names }
  9. end
  10. private
  11. def notify_author?
  12. Queries::UsersByVolumeQuery.email_notifications(eventable).exists?(poll.author_id)
  13. end
  14. def notification_recipients
  15. User.where(id: eventable.author_id)
  16. end
  17. end

app/models/events/poll_reminder.rb

0.0% lines covered

20 relevant lines. 0 lines covered and 20 lines missed.
    
  1. class Events::PollReminder < Event
  2. include Events::Notify::InApp
  3. include Events::Notify::ByEmail
  4. include Events::Notify::Chatbots
  5. def self.publish!(
  6. poll:,
  7. actor:,
  8. recipient_user_ids: [],
  9. recipient_chatbot_ids: [],
  10. recipient_message: nil,
  11. recipient_audience: nil)
  12. super(poll,
  13. discussion_id: nil,
  14. user: actor,
  15. recipient_user_ids: recipient_user_ids,
  16. recipient_chatbot_ids: recipient_chatbot_ids,
  17. recipient_audience: recipient_audience.presence,
  18. recipient_message: recipient_message.presence)
  19. end
  20. end

app/models/events/poll_reopened.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Events::PollReopened < Event
  2. 1 include Events::Notify::Chatbots
  3. 1 def self.publish!(poll, actor)
  4. 1 create(kind: "poll_reopened",
  5. user: actor,
  6. discussion: poll.discussion,
  7. 1 eventable: poll).tap { |e| EventBus.broadcast('poll_reopened_event', e) }
  8. end
  9. end

app/models/events/reaction_created.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Events::ReactionCreated < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::LiveUpdate
  4. 1 include PrettyUrlHelper
  5. 1 def self.publish!(reaction)
  6. 4 super reaction, user: reaction.user
  7. end
  8. 1 private
  9. 1 def notification_recipients
  10. 4 return User.none if !reactable || # there is no reactable
  11. reactable.author == user || # you liked your own reactable
  12. !reactable.group.memberships.find_by(user: reactable.author) # the author has left the group
  13. 2 User.where(id: reactable.author_id)
  14. end
  15. 1 def notification_translation_values
  16. 2 super.merge(
  17. reaction: eventable.reaction.downcase,
  18. model: I18n.t(:"notification_models.#{reactable.class.to_s.downcase}")
  19. )
  20. end
  21. 1 def reactable
  22. 18 @reactable ||= eventable&.reactable
  23. end
  24. end

app/models/events/stance_created.rb

86.36% lines covered

22 relevant lines. 19 lines covered and 3 lines missed.
    
  1. 1 class Events::StanceCreated < Event
  2. 1 include Events::LiveUpdate
  3. 1 include Events::Notify::ByEmail
  4. 1 include Events::Notify::InApp
  5. 1 include Events::Notify::Mentions
  6. 1 include Events::Notify::Chatbots
  7. 1 def self.publish!(stance)
  8. 68 GenericWorker.perform_async('NotificationService', 'mark_as_read', "Poll", stance.poll_id, stance.participant_id)
  9. 68 super stance,
  10. user: stance.participant.presence,
  11. 68 discussion: stance.add_to_discussion? ? stance.poll.discussion : nil
  12. end
  13. 1 def notify_mentions!
  14. 68 return if eventable.poll.anonymous || eventable.poll.hide_results == 'until_closed'
  15. 37 super
  16. end
  17. 1 def real_user
  18. eventable.real_participant
  19. end
  20. 1 private
  21. 1 def notification_translation_values
  22. {
  23. name: eventable.participant.name,
  24. title: eventable.poll.title
  25. }
  26. end
  27. 1 def notification_url
  28. @notification_url ||= polymorphic_url(eventable.poll)
  29. end
  30. 1 def email_recipients
  31. 71 Queries::UsersByVolumeQuery.loud(eventable.poll)
  32. .where.not(id: eventable.author)
  33. .where.not(id: eventable.mentioned_users).distinct
  34. end
  35. end

app/models/events/stance_updated.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class Events::StanceUpdated < Events::StanceCreated
  2. end

app/models/events/unknown_sender.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. 1 class Events::UnknownSender < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(received_email)
  5. 2 super received_email
  6. end
  7. 1 def wait_time
  8. 0.minute
  9. end
  10. 1 private
  11. 1 def notification_recipients
  12. 4 eventable.group.admins.active
  13. end
  14. 1 def email_recipients
  15. 2 Queries::UsersByVolumeQuery.
  16. email_notifications(eventable.group).
  17. where(id: notification_recipients.pluck(:id))
  18. end
  19. 1 def notification_actor
  20. nil
  21. end
  22. 1 def notification_translation_values
  23. 2 { name: notification_actor&.name, title: eventable.group.full_name }
  24. end
  25. end

app/models/events/user_added_to_group.rb

72.73% lines covered

11 relevant lines. 8 lines covered and 3 lines missed.
    
  1. 1 class Events::UserAddedToGroup < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(membership, inviter)
  5. super membership, user: inviter
  6. end
  7. 1 private
  8. 1 def notification_recipients
  9. User.where(id: eventable.user_id)
  10. end
  11. 1 alias :email_recipients :notification_recipients
  12. 1 def notification_actor
  13. eventable.inviter
  14. end
  15. end

app/models/events/user_joined_group.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class Events::UserJoinedGroup < Event
  2. def self.publish!(membership)
  3. super membership, user: membership.user
  4. end
  5. end

app/models/events/user_mentioned.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Events::UserMentioned < Event
  2. 1 include Events::Notify::InApp
  3. 1 include Events::Notify::ByEmail
  4. 1 def self.publish!(model, actor, users)
  5. 27 super model,
  6. user: actor,
  7. custom_fields: { user_ids: users.pluck(:id) }
  8. end
  9. 1 private
  10. 1 def email_recipients
  11. 28 notification_recipients.where(email_when_mentioned: true)
  12. end
  13. 1 def notification_recipients
  14. 56 User.where(id: custom_fields['user_ids'])
  15. end
  16. end

app/models/events/user_reactivated.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class Events::UserReactivated < Event
  2. include Events::Notify::ByEmail
  3. def self.publish!(user)
  4. super user, user: user
  5. end
  6. def email_users!
  7. eventable.send(:mailer).delay(queue: :critical).send(email_method, user, self)
  8. end
  9. end

app/models/formal_group.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class FormalGroup < Group
  2. end

app/models/forward_email_rule.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class ForwardEmailRule < ApplicationRecord
  2. def self.ransackable_attributes(auth_object = nil)
  3. ["email", "handle", "id"]
  4. end
  5. end

app/models/global_message_channel.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. 1 class GlobalMessageChannel
  2. 1 include Singleton
  3. 1 def message_channel
  4. '/global'
  5. end
  6. end

app/models/group.rb

0.0% lines covered

399 relevant lines. 0 lines covered and 399 lines missed.
    
  1. class Group < ApplicationRecord
  2. include HasTimeframe
  3. include HasRichText
  4. include CustomCounterCache::Model
  5. include ReadableUnguessableUrls
  6. include SelfReferencing
  7. include MessageChannel
  8. include GroupPrivacy
  9. include HasEvents
  10. include Translatable
  11. extend HasTokens
  12. extend NoSpam
  13. is_rich_text on: :description
  14. is_translatable on: :description
  15. initialized_with_token :token
  16. no_spam_for :name, :description
  17. belongs_to :creator, class_name: 'User'
  18. alias_method :author, :creator
  19. belongs_to :parent, class_name: 'Group'
  20. scope :dangling, -> { joins('left join groups parents on parents.id = groups.parent_id').where('groups.parent_id is not null and parents.id is null') }
  21. scope :empty_no_subscription, -> { joins('left join subscriptions on subscription_id = groups.subscription_id').where('subscriptions.id is null and groups.parent_id is null').where('memberships_count < 2 AND discussions_count < 3 and polls_count < 2 and subgroups_count = 0').where('groups.created_at < ?', 1.year.ago) }
  22. scope :expired_trial, -> { joins(:subscription).where('subscriptions.plan = ?', 'trial').where('subscriptions.expires_at < ?', 12.months.ago) }
  23. scope :any_trial, -> { joins(:subscription).where('subscriptions.plan = ?', 'trial') }
  24. scope :expired_demo, -> { joins(:subscription).where('subscriptions.plan = ?', 'demo').where('groups.created_at < ?', 7.days.ago) }
  25. scope :not_demo, -> { joins(:subscription).where('subscriptions.plan != ?', 'demo') }
  26. has_many :discussions, dependent: :destroy
  27. has_many :discussion_templates, dependent: :destroy
  28. has_many :public_discussions, -> { visible_to_public }, foreign_key: :group_id, class_name: 'Discussion'
  29. has_many :comments, through: :discussions
  30. has_many :all_memberships, dependent: :destroy, class_name: 'Membership'
  31. has_many :all_members, through: :all_memberships, source: :user
  32. has_many :memberships, -> { active }
  33. has_many :members, through: :memberships, source: :user
  34. has_many :accepted_memberships, -> { active.accepted }, class_name: "Membership"
  35. has_many :accepted_members, through: :accepted_memberships, source: :user
  36. has_many :admin_memberships, -> { active.where(admin: true) }, class_name: 'Membership'
  37. has_many :admins, through: :admin_memberships, source: :user
  38. has_many :membership_requests, dependent: :destroy
  39. has_many :pending_membership_requests, -> { where response: nil }, class_name: 'MembershipRequest'
  40. has_many :polls, dependent: :destroy
  41. has_many :poll_templates, dependent: :destroy
  42. has_many :documents, as: :model, dependent: :destroy
  43. has_many :requested_users, through: :membership_requests, source: :user
  44. has_many :comments, through: :discussions
  45. has_many :public_comments, through: :public_discussions, source: :comments
  46. has_many :group_identities, dependent: :destroy, foreign_key: :group_id
  47. has_many :identities, through: :group_identities
  48. has_many :chatbots, dependent: :destroy
  49. has_many :discussion_documents, through: :discussions, source: :documents
  50. has_many :poll_documents, through: :polls, source: :documents
  51. has_many :comment_documents, through: :comments, source: :documents
  52. has_many :tags, foreign_key: :group_id
  53. belongs_to :subscription
  54. has_many :subgroups,
  55. -> { where(archived_at: nil) },
  56. class_name: 'Group',
  57. foreign_key: 'parent_id'
  58. has_many :all_subgroups, dependent: :destroy, class_name: 'Group', foreign_key: :parent_id
  59. include GroupExportRelations
  60. scope :with_serializer_includes, -> { includes(:subscription) }
  61. scope :archived, -> { where('archived_at IS NOT NULL') }
  62. scope :published, -> { where(archived_at: nil) }
  63. scope :parents_only, -> { where(parent_id: nil) }
  64. scope :visible_to_public, -> { published.where(is_visible_to_public: true) }
  65. scope :hidden_from_public, -> { published.where(is_visible_to_public: false) }
  66. scope :in_organisation, ->(group) { where(id: group.id_and_subgroup_ids) }
  67. scope :explore_search, ->(query) { where("name ilike :q or description ilike :q", q: "%#{query}%") }
  68. scope :by_slack_team, ->(team_id) {
  69. joins(:identities)
  70. .where("(omniauth_identities.custom_fields->'slack_team_id')::jsonb ? :team_id", team_id: team_id)
  71. }
  72. scope :by_slack_channel, ->(channel_id) {
  73. joins(:group_identities)
  74. .where("(group_identities.custom_fields->'slack_channel_id')::jsonb ? :channel_id", channel_id: channel_id)
  75. }
  76. scope :search_for, ->(query) { where("name ilike :q", q: "%#{query}%") }
  77. validates_presence_of :name
  78. validates :name, length: { maximum: 250 }
  79. validate :limit_inheritance
  80. validates :subscription, absence: true, if: :is_subgroup?
  81. validate :handle_is_valid
  82. validates :handle, uniqueness: true, allow_nil: true
  83. delegate :locale, to: :creator, allow_nil: true
  84. delegate :time_zone, to: :creator, allow_nil: true
  85. delegate :date_time_pref, to: :creator, allow_nil: true
  86. define_counter_cache(:polls_count) { |g| g.polls.count }
  87. define_counter_cache(:closed_polls_count) { |g| g.polls.closed.count }
  88. define_counter_cache(:poll_templates_count) { |g| g.poll_templates.kept.count }
  89. define_counter_cache(:memberships_count) { |g| g.memberships.count }
  90. define_counter_cache(:pending_memberships_count) { |g| g.memberships.pending.count }
  91. define_counter_cache(:admin_memberships_count) { |g| g.admin_memberships.count }
  92. define_counter_cache(:public_discussions_count) { |g| g.discussions.visible_to_public.count }
  93. define_counter_cache(:discussions_count) { |g| g.discussions.kept.count }
  94. define_counter_cache(:open_discussions_count) { |g| g.discussions.is_open.count }
  95. define_counter_cache(:closed_discussions_count) { |g| g.discussions.is_closed.count }
  96. define_counter_cache(:discussion_templates_count) { |g| g.discussion_templates.kept.count }
  97. define_counter_cache(:subgroups_count) { |g| g.subgroups.published.count }
  98. update_counter_cache(:parent, :subgroups_count)
  99. delegate :include?, to: :users, prefix: true
  100. delegate :members, to: :parent, prefix: true
  101. has_one_attached :cover_photo, dependent: :detach
  102. has_one_attached :logo, dependent: :detach
  103. has_paper_trail only: [:name,
  104. :parent_id,
  105. :description,
  106. :description_format,
  107. :handle,
  108. :archived_at,
  109. :parent_members_can_see_discussions,
  110. :key,
  111. :is_visible_to_public,
  112. :is_visible_to_parent_members,
  113. :discussion_privacy_options,
  114. :members_can_add_members,
  115. :membership_granted_upon,
  116. :members_can_edit_discussions,
  117. :members_can_edit_comments,
  118. :members_can_delete_comments,
  119. :members_can_raise_motions,
  120. :members_can_start_discussions,
  121. :members_can_create_subgroups,
  122. :creator_id,
  123. :subscription_id,
  124. :members_can_announce,
  125. :new_threads_max_depth,
  126. :new_threads_newest_first,
  127. :admins_can_edit_user_content,
  128. :listed_in_explore]
  129. validates :description, length: { maximum: Rails.application.secrets.max_message_length }
  130. before_validation :ensure_handle_is_not_empty
  131. def logo_url(size = 512)
  132. return nil unless logo.attached?
  133. size = size.to_i
  134. Rails.application.routes.url_helpers.rails_representation_path(
  135. logo.representation(resize_to_limit: [size,size], saver: {quality: 80, strip: true}),
  136. only_path: true
  137. )
  138. rescue ActiveStorage::UnrepresentableError
  139. self.cover_photo.delete
  140. nil
  141. end
  142. def cover_url(size = 512) # 2048x512 or 1024x256 normal res
  143. size = size.to_i
  144. return nil unless cover_photo.attached?
  145. Rails.application.routes.url_helpers.rails_representation_path(
  146. cover_photo.representation(HasRichText::PREVIEW_OPTIONS.merge(resize_to_limit: [size*4,size])),
  147. only_path: true
  148. )
  149. rescue ActiveStorage::UnrepresentableError
  150. self.cover_photo.delete
  151. nil
  152. end
  153. def self_or_parent_logo_url(size = 512)
  154. logo_url(size) || (parent && parent.logo_url(size))
  155. end
  156. def self_or_parent_cover_url(size = 512)
  157. cover_url(size) || (parent && parent.cover_url(size))
  158. end
  159. def existing_member_ids
  160. member_ids
  161. end
  162. def author_id
  163. creator_id
  164. end
  165. def user_id
  166. creator_id
  167. end
  168. def discussion_id
  169. nil
  170. end
  171. def accepted_memberships_count
  172. memberships_count - pending_memberships_count
  173. end
  174. def poll_id
  175. nil
  176. end
  177. def poll
  178. nil
  179. end
  180. def title
  181. name
  182. end
  183. def guests
  184. User.none
  185. end
  186. def message_channel
  187. "/group-#{self.key}"
  188. end
  189. def parent_or_self
  190. parent || self
  191. end
  192. def self_and_subgroups
  193. Group.where(id: [id].concat(subgroup_ids))
  194. end
  195. def add_member!(user, inviter: nil)
  196. save! unless persisted?
  197. user.save! unless user.persisted?
  198. if membership = Membership.find_by(user_id: user.id, group_id: id)
  199. if membership.revoked_at
  200. membership.update(admin: false, revoked_at: nil, revoker_id: nil, accepted_at: DateTime.now, inviter: inviter)
  201. end
  202. else
  203. membership = Membership.create!(user_id: user.id, group_id: id, inviter: inviter, accepted_at: DateTime.now)
  204. end
  205. GenericWorker.perform_async('PollService', 'group_members_added', self.id)
  206. membership
  207. rescue ActiveRecord::RecordNotUnique
  208. retry
  209. end
  210. def membership_for(user)
  211. memberships.find_by(user_id: user.id)
  212. end
  213. def add_members!(users, inviter: nil)
  214. users.map { |user| add_member!(user, inviter: inviter) }
  215. end
  216. def add_admin!(user)
  217. add_member!(user).tap do |m|
  218. m.make_admin!
  219. update(creator: user) if creator.blank?
  220. end.reload
  221. end
  222. def ensure_handle_is_not_empty
  223. self.handle = nil if self.handle.to_s.strip == ""
  224. end
  225. def archive!
  226. Group.where(id: id_and_subgroup_ids).update_all(archived_at: DateTime.now)
  227. reload
  228. end
  229. def unarchive!
  230. Group.where(id: id_and_subgroup_ids).update_all(archived_at: nil)
  231. reload
  232. end
  233. def org_members_count
  234. Membership.active.where(group_id: id_and_subgroup_ids).count('distinct user_id')
  235. end
  236. def org_accepted_members_count
  237. Membership.active.accepted.where(group_id: id_and_subgroup_ids).count('distinct user_id')
  238. end
  239. def org_discussions_count
  240. Group.where(id: id_and_subgroup_ids).sum(:discussions_count)
  241. end
  242. def org_polls_count
  243. Group.where(id: id_and_subgroup_ids).sum(:polls_count)
  244. end
  245. def is_trial_or_demo?
  246. parent_group = parent_or_self
  247. subscription = Subscription.for(parent_group)
  248. ['trial', 'demo'].include?(subscription.plan)
  249. end
  250. def is_subgroup_of_hidden_parent?
  251. is_subgroup? and parent.is_hidden_from_public?
  252. end
  253. def is_parent?
  254. parent_id.blank?
  255. end
  256. def is_subgroup?
  257. !is_parent?
  258. end
  259. def admin_email
  260. admins.first.email
  261. end
  262. def full_name
  263. if is_subgroup?
  264. [parent&.name, name].compact.join(' - ')
  265. else
  266. name
  267. end
  268. end
  269. def id_and_subgroup_ids
  270. subgroup_ids.concat([id]).compact.uniq
  271. end
  272. def identity_for(type)
  273. group_identities.joins(:identity).find_by("omniauth_identities.identity_type": type)
  274. end
  275. def poll_template_positions
  276. self[:info]['poll_template_positions'] ||= {
  277. 'check' => 1,
  278. 'advice' => 2,
  279. 'consent' => 3,
  280. 'consensus' => 4,
  281. 'poll' => 5,
  282. 'score' => 6,
  283. 'dot_vote' => 7,
  284. 'ranked_choice' => 8,
  285. 'meeting' => 9,
  286. }
  287. self[:info]['poll_template_positions']
  288. end
  289. def categorize_poll_templates
  290. if self[:info].has_key? 'categorize_poll_templates'
  291. self[:info]['categorize_poll_templates']
  292. else
  293. true
  294. end
  295. end
  296. def category=(val)
  297. self[:info]['category'] = val
  298. end
  299. def category
  300. self[:info]['category']
  301. end
  302. def categorize_poll_templates=(val)
  303. self[:info]['categorize_poll_templates'] = val
  304. end
  305. def hidden_poll_templates
  306. self[:info]['hidden_poll_templates'] ||= AppConfig.app_features.fetch(:hidden_poll_templates, [])
  307. self[:info]['hidden_poll_templates']
  308. end
  309. def hidden_poll_templates=(val)
  310. self[:info]['hidden_poll_templates'] = val
  311. end
  312. def self.ransackable_attributes(auth_object = nil)
  313. [
  314. "admin_memberships_count",
  315. "admin_tags",
  316. "admins_can_edit_user_content",
  317. "archived_at",
  318. "attachments",
  319. "category_id",
  320. "city",
  321. "closed_discussions_count",
  322. "closed_motions_count",
  323. "closed_polls_count",
  324. "cohort_id",
  325. "content_locale",
  326. "country",
  327. "cover_photo_content_type",
  328. "cover_photo_file_name",
  329. "cover_photo_file_size",
  330. "cover_photo_updated_at",
  331. "created_at",
  332. "creator_id",
  333. "default_group_cover_id",
  334. "description",
  335. "description_format",
  336. "discussion_privacy_options",
  337. "discussions_count",
  338. "full_name",
  339. "handle",
  340. "id",
  341. "invitations_count",
  342. "is_referral",
  343. "is_visible_to_parent_members",
  344. "is_visible_to_public",
  345. "key",
  346. "listed_in_explore",
  347. "logo_content_type",
  348. "logo_file_name",
  349. "logo_file_size",
  350. "logo_updated_at",
  351. "members_can_add_guests",
  352. "members_can_add_members",
  353. "members_can_announce",
  354. "members_can_create_subgroups",
  355. "members_can_delete_comments",
  356. "members_can_edit_comments",
  357. "members_can_edit_discussions",
  358. "members_can_raise_motions",
  359. "members_can_start_discussions",
  360. "members_can_vote",
  361. "membership_granted_upon",
  362. "memberships_count",
  363. "name",
  364. "new_threads_max_depth",
  365. "new_threads_newest_first",
  366. "open_discussions_count",
  367. "parent_id",
  368. "parent_members_can_see_discussions",
  369. "pending_memberships_count",
  370. "poll_templates_count",
  371. "polls_count",
  372. "proposal_outcomes_count",
  373. "public_discussions_count",
  374. "recent_activity_count",
  375. "region",
  376. "subgroups_count",
  377. "subscription_id",
  378. "template_discussions_count",
  379. "theme_id",
  380. "updated_at"]
  381. end
  382. private
  383. def variant_path(variant)
  384. Rails.application.routes.url_helpers.rails_representation_path(variant, only_path: true)
  385. end
  386. def handle_is_valid
  387. self.handle = nil if self.handle.to_s.strip == "" || (is_subgroup? && parent.handle.nil?)
  388. return if handle.nil?
  389. self.handle = handle.parameterize
  390. if is_subgroup? && parent.handle && !handle.starts_with?("#{parent.handle}-")
  391. errors.add(:handle, I18n.t(:'group.error.handle_must_begin_with_parent_handle', parent_handle: parent.handle))
  392. end
  393. end
  394. def limit_inheritance
  395. if parent_id.present?
  396. errors[:base] << "Can't set a subgroup as parent" unless parent.parent_id.nil?
  397. end
  398. end
  399. end

app/models/group_identity.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 class GroupIdentity < ApplicationRecord
  2. 1 extend HasCustomFields
  3. 1 attr_writer :make_announcement
  4. 1 def make_announcement
  5. !!@make_announcement
  6. end
  7. 1 attr_accessor :webhook_url
  8. 1 belongs_to :group, class_name: 'Group', required: true
  9. 1 belongs_to :identity, class_name: 'Identities::Base', required: true, dependent: :destroy
  10. 1 set_custom_fields :slack_channel_id, :slack_channel_name
  11. 1 attr_accessor :identity_type
  12. 1 delegate :slack_team_name, to: :identity
  13. 1 delegate :slack_team_id, to: :identity
  14. 1 delegate :title, to: :group
  15. end

app/models/group_survey.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class GroupSurvey < ApplicationRecord
  2. belongs_to :group, class_name: 'Group', foreign_key: :group_id
  3. has_one :subscription, through: :group, source: :subscription
  4. end

app/models/guest_group.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class GuestGroup < Group
  2. end

app/models/identities/base.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. class Identities::Base < ApplicationRecord
  2. extend HasCustomFields
  3. self.table_name = :omniauth_identities
  4. validates :identity_type, presence: true
  5. validates :access_token, presence: true, if: :requires_access_token?
  6. validates :uid, presence: true
  7. belongs_to :user, required: false
  8. PROVIDERS = YAML.load_file(Rails.root.join("config", "providers.yml"))['identity']
  9. discriminate Identities, on: :identity_type
  10. scope :with_user, -> { where.not(user: nil) }
  11. scope :slack, -> { where(identity_type: :slack) }
  12. def self.set_identity_type(type)
  13. after_initialize { self.identity_type = type }
  14. end
  15. def find_or_create_user!
  16. User.find_or_create_by(email: self.email).associate_with_identity(self)
  17. end
  18. def assign_logo!
  19. return unless user && logo
  20. user.uploaded_avatar.attach(
  21. io: URI.open(URI.parse(logo)),
  22. filename: File.basename(logo)
  23. )
  24. user.update(avatar_kind: :uploaded)
  25. rescue OpenURI::HTTPError, TypeError
  26. # Can't load logo uri as attachment; do nothing
  27. end
  28. def requires_access_token?
  29. true
  30. end
  31. end

app/models/identities/facebook.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. class Identities::Facebook < Identities::Base
  2. include Identities::WithClient
  3. set_identity_type :facebook
  4. def apply_user_info(payload)
  5. self.uid ||= payload['id']
  6. self.name ||= payload['name']
  7. self.email ||= payload['email']
  8. end
  9. def fetch_user_avatar
  10. self.logo = client.fetch_user_avatar(self.uid).json
  11. end
  12. def admin_groups
  13. if permissions_response.json['error'].blank?
  14. client.fetch_admin_groups(self.uid)
  15. else
  16. permissions_response
  17. end
  18. end
  19. private
  20. def publish_events
  21. []
  22. end
  23. def permissions_response
  24. @permission_response ||= client.fetch_permissions(self.uid)
  25. end
  26. end

app/models/identities/google.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class Identities::Google < Identities::Base
  2. include Identities::WithClient
  3. set_identity_type :google
  4. def apply_user_info(payload)
  5. self.uid ||= payload['id']
  6. self.name ||= payload['name']
  7. self.email ||= payload['email']
  8. self.logo ||= payload['picture']
  9. end
  10. end

app/models/identities/nextcloud.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class Identities::Nextcloud < Identities::Base
  2. include Identities::WithClient
  3. set_identity_type :nextcloud
  4. def apply_user_info(payload)
  5. payload = payload['ocs']['data']
  6. self.uid ||= payload['id']
  7. self.name ||= payload['id']
  8. self.email ||= payload['email']
  9. end
  10. end

app/models/identities/oauth.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class Identities::Oauth < Identities::Base
  2. include Identities::WithClient
  3. set_identity_type :oauth
  4. def apply_user_info(payload)
  5. self.uid ||= payload.dig(ENV.fetch('OAUTH_ATTR_UID'))
  6. self.name ||= payload.dig(ENV.fetch('OAUTH_ATTR_NAME'))
  7. self.email ||= payload.dig(ENV.fetch('OAUTH_ATTR_EMAIL'))
  8. end
  9. end

app/models/identities/saml.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. class Identities::Saml < Identities::Base
  2. include Routing
  3. set_identity_type :saml
  4. attr_accessor :response
  5. def settings
  6. @settings ||= begin
  7. if ENV['SAML_IDP_METADATA']
  8. settings = OneLogin::RubySaml::IdpMetadataParser.new.parse(ENV['SAML_IDP_METADATA'])
  9. else
  10. settings = OneLogin::RubySaml::IdpMetadataParser.new.parse_remote(ENV['SAML_IDP_METADATA_URL'])
  11. end
  12. settings.assertion_consumer_service_url = saml_oauth_url
  13. settings.issuer = ENV.fetch('SAML_ISSUER', saml_metadata_url)
  14. settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
  15. settings
  16. end
  17. end
  18. def fetch_user_info
  19. return unless self.response.is_valid?
  20. self.email = self.uid = self.response.nameid
  21. self.name = self.response.attributes['displayName']
  22. end
  23. def requires_access_token?
  24. false
  25. end
  26. end

app/models/logged_out_user.rb

84.09% lines covered

44 relevant lines. 37 lines covered and 7 lines missed.
    
  1. 1 class LoggedOutUser
  2. 1 include Null::User
  3. 1 include AvatarInitials
  4. 1 attr_accessor :name, :email, :token, :avatar_initials, :locale, :legal_accepted, :recaptcha, :time_zone, :date_time_pref, :autodetect_time_zone
  5. 1 alias :read_attribute_for_serialization :send
  6. 1 def tags
  7. Tag.none
  8. end
  9. 1 def initialize(name: nil, email: nil, token: nil, locale: I18n.locale, time_zone: 'UTC', date_time_pref: 'day_abbr', params: {}, session: {})
  10. 3305 @name = name
  11. 3305 @email = email
  12. 3305 @token = token
  13. 3305 @locale = locale
  14. 3305 @date_time_pref = date_time_pref
  15. 3305 @time_zone = time_zone
  16. 3305 @autodetect_time_zone = true
  17. 3305 @params = params
  18. 3305 @session = session
  19. 3305 apply_null_methods!
  20. 3305 set_avatar_initials if (@name || @email)
  21. end
  22. 1 def name_or_username
  23. @name || @username
  24. end
  25. 1 def group_token
  26. 4 @params[:group_token] || @session[:pending_group_token]
  27. end
  28. 1 def membership_token
  29. 3 @params[:membership_token] || @session[:pending_membership_token]
  30. end
  31. 1 def stance_token
  32. 1 @params[:stance_token]
  33. end
  34. 1 def discussion_reader_token
  35. 10 @params[:discussion_reader_token]
  36. end
  37. 1 def create_user
  38. User.create(name: name,
  39. email: email,
  40. token: token,
  41. legal_accepted: legal_accepted,
  42. require_valid_signup: true,
  43. recaptcha: recaptcha)
  44. end
  45. 1 def memberships_count
  46. 0
  47. end
  48. 1 def message_channel
  49. nil
  50. end
  51. 1 def nil_methods
  52. 3305 super + [:id, :created_at, :avatar_url, :thumb_url, :presence, :restricted, :persisted?, :secret_token, :content_locale, :browseable_group_ids]
  53. end
  54. 1 def false_methods
  55. 3305 super + [:save, :persisted?]
  56. end
  57. 1 def errors
  58. ActiveModel::Errors.new self
  59. end
  60. 1 def email_status
  61. User.email_status_for(self.email)
  62. end
  63. 1 def avatar_kind
  64. 'initials'
  65. end
  66. end

app/models/login_token.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 class LoginToken < ApplicationRecord
  2. 1 belongs_to :user, required: true
  3. 1 extend HasTokens
  4. 1 initialized_with_token :token
  5. 28 initialized_with_token :code, -> { generate_code }
  6. 1 EXPIRATION = ENV.fetch('LOGIN_TOKEN_EXPIRATION_MINUTES', 1440)
  7. 1 scope :unused, -> { where(used: false) }
  8. 1 def useable?
  9. 8 !used && expires_at > DateTime.now && user.present?
  10. end
  11. 1 def expires_at
  12. 6 self.created_at + EXPIRATION.minutes
  13. end
  14. 1 def user
  15. 46 User.verified.find_by(email: super.email) || super
  16. end
  17. 1 def self.generate_code
  18. 27 code = 0
  19. 27 while code < 100000
  20. 31 code = Random.new.rand(999999)
  21. end
  22. 27 code
  23. end
  24. end

app/models/member_email_alias.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class MemberEmailAlias < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 belongs_to :group
  4. 1 belongs_to :author, class_name: "User"
  5. 8 scope :blocked, -> { where(user_id: nil) }
  6. 6 scope :allowed, -> { where.not(user_id: nil) }
  7. end

app/models/membership.rb

0.0% lines covered

75 relevant lines. 0 lines covered and 75 lines missed.
    
  1. class Membership < ApplicationRecord
  2. class InvitationAlreadyUsed < StandardError
  3. attr_accessor :membership
  4. def initialize(obj)
  5. self.membership = obj
  6. end
  7. end
  8. include CustomCounterCache::Model
  9. include HasVolume
  10. include HasTimeframe
  11. include HasExperiences
  12. scope :in_organisation, -> (group) { includes(:user).where(group_id: group.id_and_subgroup_ids) }
  13. extend FriendlyId
  14. extend HasTokens
  15. friendly_id :token
  16. initialized_with_token :token
  17. validates_presence_of :group, :user
  18. validates_uniqueness_of :user_id, scope: :group_id
  19. belongs_to :group
  20. belongs_to :user
  21. belongs_to :inviter, class_name: 'User'
  22. belongs_to :revoker, class_name: 'User'
  23. has_many :events, as: :eventable, dependent: :destroy
  24. scope :dangling, -> { joins('left join groups g on memberships.group_id = g.id').where('group_id is not null and g.id is null') }
  25. scope :active, -> { where(revoked_at: nil) }
  26. scope :pending, -> { active.where(accepted_at: nil) }
  27. scope :accepted, -> { where('accepted_at IS NOT NULL') }
  28. scope :revoked, -> { where('revoked_at IS NOT NULL') }
  29. scope :search_for, ->(query) { joins(:user).where("users.name ilike :query or users.username ilike :query or users.email ilike :query", query: "%#{query}%") }
  30. scope :email_verified, -> { joins(:user).where("users.email_verified": true) }
  31. scope :for_group, lambda {|group| where(group_id: group)}
  32. scope :admin, -> { where(admin: true) }
  33. has_paper_trail only: [:group_id, :user_id, :inviter_id, :admin, :title, :revoked_at, :revoker_id, :volume, :accepted_at]
  34. delegate :name, :email, to: :user, prefix: :user, allow_nil: true
  35. delegate :parent, to: :group, prefix: :group, allow_nil: true
  36. delegate :name, :full_name, to: :group, prefix: :group
  37. delegate :admins, to: :group, prefix: :group
  38. delegate :name, to: :inviter, prefix: :inviter, allow_nil: true
  39. delegate :mailer, to: :user
  40. update_counter_cache :group, :memberships_count
  41. update_counter_cache :group, :pending_memberships_count
  42. update_counter_cache :group, :admin_memberships_count
  43. update_counter_cache :user, :memberships_count
  44. before_create :set_volume
  45. def author_id
  46. inviter_id
  47. end
  48. def author
  49. inviter
  50. end
  51. def message_channel
  52. "membership-#{token}"
  53. end
  54. def make_admin!
  55. update_attribute(:admin, true)
  56. end
  57. def remove_admin!
  58. update_attribute(:admin, false)
  59. end
  60. def discussion_readers
  61. DiscussionReader.
  62. joins(:discussion).
  63. where("discussions.group_id": group_id).
  64. where("discussion_readers.user_id": user_id)
  65. end
  66. def stances
  67. Stance.joins(:poll).
  68. where("polls.group_id": group_id).
  69. where(participant_id: user_id)
  70. end
  71. private
  72. def set_volume
  73. self.volume = user.default_membership_volume if id.nil?
  74. end
  75. end

app/models/membership_request.rb

94.83% lines covered

58 relevant lines. 55 lines covered and 3 lines missed.
    
  1. 1 class MembershipRequest < ApplicationRecord
  2. 1 include HasEvents
  3. 1 validate :validate_not_in_group_already
  4. 1 validate :validate_unique_membership_request
  5. 1 validates_presence_of :responder, if: :response
  6. 1 validates :group, presence: true
  7. 1 validates_length_of :introduction, maximum: 250, unless: :persisted?
  8. 1 belongs_to :group
  9. 1 belongs_to :requestor, class_name: 'User'
  10. 1 belongs_to :user, foreign_key: 'requestor_id' # duplicate relationship for eager loading
  11. 1 belongs_to :responder, class_name: 'User'
  12. 1 has_many :admins, through: :group
  13. 1 validates :introduction, length: { maximum: Rails.application.secrets.max_message_length }
  14. 1 scope :dangling, -> { joins('left join groups on groups.id = group_id').where('groups.id is null') }
  15. 2 scope :pending, -> { where(response: nil).order('created_at DESC') }
  16. 2 scope :responded_to, -> { where('response IS NOT ?', nil).order('responded_at DESC') }
  17. 1 scope :requested_by, ->(user) { where requestor_id: user.id }
  18. 1 delegate :members, to: :group, prefix: true
  19. 1 delegate :membership_requests, to: :group, prefix: true
  20. 1 delegate :members_can_add_members, to: :group, prefix: true
  21. 1 delegate :name, to: :group, prefix: true
  22. 1 delegate :mailer, to: :group
  23. 1 delegate :email, to: :requestor, allow_nil: true
  24. 1 delegate :name, to: :requestor, allow_nil: true
  25. 1 def author_id
  26. requestor_id
  27. end
  28. 1 def title
  29. group.full_name
  30. end
  31. 1 def user_id
  32. requestor_id
  33. end
  34. 1 def approve!(responder)
  35. 12 set_response_details('approved', responder)
  36. end
  37. 1 def ignore!(responder)
  38. 1 set_response_details('ignored', responder)
  39. end
  40. 1 def convert_to_membership!
  41. 1 group.add_member!(requestor)
  42. end
  43. 1 private
  44. 1 def validate_not_in_group_already
  45. 58 if has_not_been_saved_yet? && already_in_group?
  46. 3 add_already_in_group_error
  47. end
  48. end
  49. 1 def validate_unique_membership_request
  50. 58 if has_not_been_saved_yet? && pending_request_already_exists?
  51. 3 add_already_requested_membership_error
  52. end
  53. end
  54. 1 def has_not_been_saved_yet?
  55. 116 not persisted?
  56. end
  57. 1 def already_in_group?
  58. 46 group_members.exists?(requestor.id)
  59. end
  60. 1 def pending_request_already_exists?
  61. 46 group_membership_requests.where(requestor_id: requestor.id, response: nil).exists?
  62. end
  63. 1 def add_already_requested_membership_error
  64. 3 errors.add(:requestor, I18n.t(:'error.you_have_already_requested_membership'))
  65. end
  66. 1 def add_already_in_group_error
  67. 3 errors.add(:requestor, I18n.t(:'error.you_are_already_a_member_of_this_group'))
  68. end
  69. 1 def set_response_details(response, responder)
  70. 13 self.response = response
  71. 13 self.responder = responder
  72. 13 self.responded_at = Time.now
  73. 13 save!
  74. end
  75. end

app/models/notification.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class Notification < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 belongs_to :actor, class_name: "User"
  4. 1 belongs_to :event
  5. 1 validates_presence_of :user, :event
  6. 1 delegate :eventable, to: :event, allow_nil: true
  7. 1 delegate :kind, to: :event, allow_nil: true
  8. 1 delegate :locale, to: :user
  9. 1 delegate :message_channel, to: :user
  10. 1 scope :dangling, -> { joins('left join events e on notifications.event_id = e.id left join users u on u.id = notifications.user_id').where('e.id is null or u.id is null') }
  11. 1426 scope :user_mentions, -> { joins(:event).where("events.kind": :user_mentioned) }
  12. end

app/models/null_discussion.rb

0.0% lines covered

42 relevant lines. 0 lines covered and 42 lines missed.
    
  1. class NullDiscussion
  2. include Null::Object
  3. def initialize
  4. apply_null_methods!
  5. end
  6. def group
  7. self
  8. end
  9. alias :read_attribute_for_serialization :send
  10. def title
  11. "Null discussion"
  12. end
  13. def nil_methods
  14. %w(
  15. id
  16. key
  17. presence
  18. present?
  19. content_locale
  20. description
  21. description_format
  22. group_id
  23. message_channel
  24. created_at
  25. author_id
  26. )
  27. end
  28. def true_methods
  29. []
  30. end
  31. def empty_methods
  32. [:member_ids]
  33. end
  34. def none_methods
  35. {
  36. admins: :user,
  37. members: :user,
  38. memberships: :membership,
  39. readers: :user
  40. }
  41. end
  42. end

app/models/null_group.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class NullGroup
  2. 1 include Null::Group
  3. 1 def initialize
  4. 2795 apply_null_methods!
  5. end
  6. end

app/models/null_poll.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. class NullPoll
  2. include Null::Object
  3. def initialize
  4. apply_null_methods!
  5. end
  6. def group
  7. self
  8. end
  9. alias :read_attribute_for_serialization :send
  10. def title
  11. "Null poll"
  12. end
  13. def nil_methods
  14. %w(
  15. id
  16. key
  17. presence
  18. present?
  19. content_locale
  20. details
  21. details_format
  22. group_id
  23. message_channel
  24. created_at
  25. author_id
  26. )
  27. end
  28. def true_methods
  29. []
  30. end
  31. def empty_methods
  32. [:member_ids, :voter_ids]
  33. end
  34. def none_methods
  35. {
  36. admins: :user,
  37. members: :user,
  38. memberships: :membership,
  39. unmasked_decided_voters: :user,
  40. unmasked_undecided_voters: :user,
  41. unmasked_voters: :user,
  42. non_voters: :user,
  43. voters: :user
  44. }
  45. end
  46. end

app/models/outcome.rb

92.59% lines covered

54 relevant lines. 50 lines covered and 4 lines missed.
    
  1. 1 class Outcome < ApplicationRecord
  2. 1 include CustomCounterCache::Model
  3. 1 extend HasCustomFields
  4. 1 include HasEvents
  5. 1 include HasMentions
  6. 1 include Reactable
  7. 1 include Translatable
  8. 1 include HasCreatedEvent
  9. 1 include HasEvents
  10. 1 include HasRichText
  11. 1 include Searchable
  12. 1 def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil, poll_id: nil)
  13. 149 content_str = "regexp_replace(CONCAT_WS(' ', outcomes.statement, users.name), E'<[^>]+>', '', 'gi')"
  14. 149 <<~SQL.squish
  15. INSERT INTO pg_search_documents (
  16. searchable_type,
  17. searchable_id,
  18. poll_id,
  19. group_id,
  20. discussion_id,
  21. author_id,
  22. authored_at,
  23. content,
  24. ts_content,
  25. created_at,
  26. updated_at)
  27. SELECT 'Outcome' AS searchable_type,
  28. outcomes.id AS searchable_id,
  29. outcomes.poll_id AS poll_id,
  30. polls.group_id as group_id,
  31. polls.discussion_id AS discussion_id,
  32. outcomes.author_id AS author_id,
  33. outcomes.created_at as authored_at,
  34. #{content_str} AS content,
  35. to_tsvector('simple', #{content_str}) as ts_content,
  36. now() AS created_at,
  37. now() AS updated_at
  38. FROM outcomes
  39. LEFT JOIN users ON users.id = outcomes.author_id
  40. LEFT JOIN polls ON polls.id = outcomes.poll_id
  41. WHERE polls.discarded_at IS NULL
  42. #{id ? " AND outcomes.id = #{id.to_s.to_i} LIMIT 1" : ""}
  43. #{author_id ? " AND outcomes.author_id = #{author_id.to_s.to_i}" : ""}
  44. #{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_s.to_i}" : ""}
  45. #{poll_id ? " AND outcomes.poll_id = #{poll_id.to_s.to_i}" : ""}
  46. SQL
  47. end
  48. 1 is_rich_text on: :statement
  49. 1 set_custom_fields :event_summary, :event_description, :event_location
  50. 1936 scope :latest, -> { where(latest: true) }
  51. 1 scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
  52. 1 scope :in_organisation, -> (group) { joins(:poll).where('polls.group_id': group.id_and_subgroup_ids) }
  53. 1 belongs_to :poll, required: true
  54. 1 belongs_to :poll_option, required: false
  55. 1 belongs_to :author, class_name: 'User', required: true
  56. 1 has_many :stances, through: :poll
  57. 1 has_many :documents, as: :model, dependent: :destroy
  58. %w(
  59. 1 title poll_type dates_as_options group group_id discussion discussion_id
  60. locale mailer members admins discarded? tags
  61. 13 ).each { |message| delegate message, to: :poll }
  62. 1 is_mentionable on: :statement
  63. 1 is_translatable on: :statement
  64. 1 has_paper_trail only: [:statement, :statement_format, :author_id, :review_on]
  65. 2 define_counter_cache(:versions_count) { |d| d.versions.count }
  66. 1 validates :statement, presence: true, length: { maximum: Rails.application.secrets.max_message_length }
  67. 1 validate :has_valid_poll_option
  68. 1 scope :review_due_not_published, -> (due_date) do
  69. 3 where(review_on: due_date).where("NOT EXISTS (
  70. SELECT 1 FROM events
  71. WHERE events.eventable_id = outcomes.id AND
  72. events.eventable_type = 'Outcome' AND
  73. events.kind = 'outcome_review_due')")
  74. end
  75. 1 def author_name
  76. author.name
  77. end
  78. 1 def user_id
  79. 80 author_id
  80. end
  81. 1 def body
  82. statement
  83. end
  84. 1 def body=(val)
  85. self.statement = val
  86. end
  87. 1 def body_format
  88. statement_format
  89. end
  90. 1 def parent_event
  91. 34 poll.created_event
  92. end
  93. 1 def attendee_emails
  94. 8 self.stances.joins(:participant).joins(:stance_choices)
  95. .where("stance_choices.poll_option_id": self.poll_option_id)
  96. .pluck(:"users.email").flatten.compact.uniq
  97. end
  98. 1 def calendar_invite
  99. 59 return nil unless self.poll_option && self.dates_as_options
  100. 8 CalendarInvite.new(self).to_ical
  101. end
  102. 1 def has_valid_poll_option
  103. 123 return if !self.poll_option_id || poll.poll_option_ids.include?(self.poll_option_id)
  104. 1 errors.add(:poll_option_id, I18n.t(:"outcome.error.invalid_poll_option"))
  105. end
  106. end

app/models/permitted_params.rb

79.17% lines covered

48 relevant lines. 38 lines covered and 10 lines missed.
    
  1. 1 class PermittedParams < Struct.new(:params)
  2. MODELS = %w(
  3. 1 user group membership_request membership poll poll_template outcome
  4. stance discussion discussion_template discussion_reader comment
  5. contact_message document
  6. webhook chatbot contact_request reaction tag
  7. )
  8. 1 MODELS.each do |kind|
  9. 19 define_method(kind) do
  10. 164 permitted_attributes = self.send("#{kind}_attributes")
  11. 164 params.require(kind).permit(*permitted_attributes)
  12. end
  13. 19 alias_method :"api_#{kind}", kind.to_sym
  14. end
  15. 1 alias :read_attribute_for_serialization :send
  16. 1 def user_attributes
  17. 3 [:name, :avatar_kind, :email, :password, :password_confirmation, :current_password,
  18. :remember_me, :uploaded_avatar, :username, :short_bio, :short_bio_format, :location,
  19. :autodetect_time_zone, :time_zone, :selected_locale, :email_when_mentioned, :default_membership_volume,
  20. :email_catch_up_day, :has_password, :has_token, :email_status,
  21. :email_when_proposal_closing_soon, :email_new_discussions_and_proposals, :email_on_participation, :email_newsletter,
  22. :date_time_pref, :bot,
  23. :legal_accepted, {email_new_discussions_and_proposals_group_ids: []},
  24. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  25. ]
  26. end
  27. 1 def poll_attributes
  28. [
  29. 31 :agree_target,
  30. :title,
  31. :details,
  32. :details_format,
  33. :discussion_id,
  34. :default_duration_in_days,
  35. :poll_type,
  36. :group_id,
  37. :closing_at,
  38. :anonymous,
  39. :hide_results,
  40. :key,
  41. :limit_reason_length,
  42. :shuffle_options,
  43. :notify_on_closing_soon,
  44. :voter_can_add_options,
  45. :specified_voters_only,
  46. :recipient_audience,
  47. :recipient_message,
  48. :tags, {tags: []},
  49. :notify_recipients,
  50. :recipient_user_ids, {recipient_user_ids: []},
  51. :recipient_chatbot_ids, {recipient_chatbot_ids: []},
  52. :recipient_emails, {recipient_emails: []},
  53. :can_respond_maybe,
  54. :dots_per_person,
  55. :max_score,
  56. :min_score,
  57. :options, {options: []},
  58. :process_name,
  59. :process_subtitle,
  60. :poll_option_name_format,
  61. :reason_prompt,
  62. :template,
  63. :time_zone,
  64. :stance_reason_required,
  65. :meeting_duration,
  66. :minimum_stance_choices,
  67. :maximum_stance_choices,
  68. :chart_type,
  69. :document_ids, {document_ids: []},
  70. :poll_template_id,
  71. :poll_template_key,
  72. :poll_options_attributes, {poll_options_attributes: [:id, :name, :icon, :meaning, :prompt, :priority, :_destroy]},
  73. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  74. ]
  75. end
  76. 1 def poll_template_attributes
  77. [
  78. :key,
  79. :group_id,
  80. :position,
  81. :author_id,
  82. :poll_type,
  83. :process_name,
  84. :process_subtitle,
  85. :process_introduction,
  86. :process_introduction_format,
  87. :title,
  88. :title_placeholder,
  89. :details,
  90. :details_format,
  91. :notify_on_participate,
  92. :anonymous,
  93. :specified_voters_only,
  94. :notify_on_closing_soon,
  95. :content_locale,
  96. :shuffle_options,
  97. :hide_results,
  98. :chart_type,
  99. :min_score,
  100. :max_score,
  101. :minimum_stance_choices,
  102. :maximum_stance_choices,
  103. :dots_per_person,
  104. :reason_prompt,
  105. :tags, {tags: []},
  106. :poll_options, {poll_options: [:name, :icon, :meaning, :prompt, :priority]},
  107. :stance_reason_required,
  108. :limit_reason_length,
  109. :default_duration_in_days,
  110. :agree_target,
  111. :meeting_duration,
  112. :can_respond_maybe,
  113. :poll_option_name_format,
  114. :outcome_statement,
  115. :outcome_statement_format,
  116. :outcome_review_due_in_days,
  117. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  118. ]
  119. end
  120. 1 def stance_attributes
  121. 18 [:poll_id, :reason, :reason_format,
  122. :stance_choices_attributes, {stance_choices_attributes: [:score, :poll_option_id]},
  123. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  124. ]
  125. end
  126. 1 def stance_choice_attributes
  127. [:score, :poll_option_id, :stance_id]
  128. end
  129. 1 def outcome_attributes
  130. 23 [:statement, :statement_format, :poll_id, :poll_option_id, :review_on, :recipient_audience, :include_actor,
  131. :event_location, :event_summary, :event_description,
  132. :notify_recipients,
  133. :recipient_user_ids, {recipient_user_ids: []},
  134. :recipient_chatbot_ids, {recipient_chatbot_ids: []},
  135. :recipient_emails, {recipient_emails: []},
  136. :document_ids, {document_ids: []},
  137. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  138. ]
  139. end
  140. 1 def membership_request_attributes
  141. [:name, :email, :introduction, :group_id]
  142. end
  143. 1 def membership_attributes
  144. 1 [:title, :volume, :apply_to_all, :set_default]
  145. end
  146. 1 def discussion_reader_attributes
  147. [:discussion_id, :volume]
  148. end
  149. 1 def group_attributes
  150. 5 [:parent_id, :name, :handle, :group_privacy, :is_visible_to_public, :discussion_privacy_options,
  151. :members_can_add_members, :members_can_add_guests, :members_can_announce,
  152. :members_can_edit_discussions, :members_can_edit_comments, :members_can_delete_comments,
  153. :description, :description_format, :is_visible_to_parent_members, :parent_members_can_see_discussions,
  154. :membership_granted_upon, :cover_photo, :logo, :category, :members_can_raise_motions,
  155. :members_can_start_discussions, :members_can_create_subgroups, :admins_can_edit_user_content,
  156. :new_threads_max_depth, :new_threads_newest_first,
  157. :document_ids, {document_ids: []},
  158. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  159. ]
  160. end
  161. 1 def webhook_attributes
  162. [:group_id, :url, :name, :format, :include_body, :include_subgroups, :permissions, :event_kinds, {event_kinds: [], permissions: []}]
  163. end
  164. 1 def chatbot_attributes
  165. [:name, :group_id, :kind, :webhook_kind, :server, :access_token, :channel, :notification_only, :event_kinds, {event_kinds: []}]
  166. end
  167. 1 def discussion_attributes
  168. 54 [:title,
  169. :description,
  170. :description_format,
  171. :discussion_template_id,
  172. :discussion_template_key,
  173. :group_id,
  174. :newest_first,
  175. :max_depth,
  176. :private,
  177. :notify_recipients,
  178. :recipient_audience,
  179. :recipient_message,
  180. :tags, {tags: []},
  181. :recipient_user_ids, {recipient_user_ids: []},
  182. :recipient_chatbot_ids, {recipient_chatbot_ids: []},
  183. :recipient_emails, {recipient_emails: []},
  184. :forked_event_ids, {forked_event_ids: []},
  185. :document_ids, {document_ids: []},
  186. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  187. ]
  188. end
  189. 1 def discussion_template_attributes
  190. [
  191. :key,
  192. :title,
  193. :title_placeholder,
  194. :description,
  195. :description_format,
  196. :process_name,
  197. :process_subtitle,
  198. :process_introduction,
  199. :process_introduction_format,
  200. :recipient_audience,
  201. :group_id,
  202. :newest_first,
  203. :max_depth,
  204. :public,
  205. :poll_template_keys_or_ids, {poll_template_keys_or_ids: []},
  206. :tags, {tags: []},
  207. :link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
  208. ]
  209. end
  210. 1 def tag_attributes
  211. 6 [:name, :color, :group_id, :priority]
  212. end
  213. 1 def comment_attributes
  214. 18 [:body, :body_format, :discussion_id, :parent_id, :parent_type,
  215. :document_ids, {document_ids: []},
  216. :link_previews, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]},
  217. :files, {files: []},
  218. :image_files, {image_files: []}]
  219. end
  220. 1 def reaction_attributes
  221. 6 [:reaction, :reactable_id, :reactable_type]
  222. end
  223. 1 def contact_message_attributes
  224. [:email, :subject, :user_id, :message, :name]
  225. end
  226. 1 def contact_request_attributes
  227. [:recipient_id, :message]
  228. end
  229. 1 def document_attributes
  230. [:url, :title, :model_id, :model_type, :file, :filename]
  231. end
  232. end

app/models/poll.rb

0.0% lines covered

440 relevant lines. 0 lines covered and 440 lines missed.
    
  1. class Poll < ApplicationRecord
  2. extend HasCustomFields
  3. include CustomCounterCache::Model
  4. include ReadableUnguessableUrls
  5. include HasEvents
  6. include HasMentions
  7. include MessageChannel
  8. include SelfReferencing
  9. include Reactable
  10. include HasCreatedEvent
  11. include HasRichText
  12. include HasTags
  13. include Discard::Model
  14. include Searchable
  15. def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
  16. content_str = "regexp_replace(CONCAT_WS(' ', polls.title, polls.details, users.name), E'<[^>]+>', '', 'gi')"
  17. <<~SQL.squish
  18. INSERT INTO pg_search_documents (
  19. searchable_type,
  20. searchable_id,
  21. poll_id,
  22. group_id,
  23. discussion_id,
  24. author_id,
  25. authored_at,
  26. content,
  27. ts_content,
  28. created_at,
  29. updated_at)
  30. SELECT 'Poll' AS searchable_type,
  31. polls.id AS searchable_id,
  32. polls.id AS poll_id,
  33. polls.group_id as group_id,
  34. polls.discussion_id AS discussion_id,
  35. polls.author_id AS author_id,
  36. polls.created_at AS authored_at,
  37. #{content_str} AS content,
  38. to_tsvector('simple', #{content_str}) as ts_content,
  39. now() AS created_at,
  40. now() AS updated_at
  41. FROM polls
  42. LEFT JOIN users ON users.id = polls.author_id
  43. WHERE polls.discarded_at IS NULL
  44. #{id ? " AND polls.id = #{id.to_i} LIMIT 1" : ""}
  45. #{author_id ? " AND polls.author_id = #{author_id.to_i}" : ""}
  46. #{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_i}" : ""}
  47. SQL
  48. end
  49. is_rich_text on: :details
  50. extend NoSpam
  51. no_spam_for :title, :details
  52. set_custom_fields :meeting_duration,
  53. :time_zone,
  54. :can_respond_maybe
  55. TEMPLATE_DEFAULT_FIELDS = %w[
  56. poll_option_name_format
  57. max_score
  58. min_score
  59. dots_per_person
  60. chart_type
  61. default_duration_in_days
  62. ]
  63. TEMPLATE_DEFAULT_FIELDS.each do |field|
  64. define_method field, -> {
  65. self[field] || self[:custom_fields][field] || AppConfig.poll_types.dig(self.poll_type, 'defaults', field)
  66. }
  67. define_method :"#{field}=", ->(value) {
  68. self[:custom_fields].delete(field)
  69. if value == AppConfig.poll_types.dig(self.poll_type, 'defaults', field)
  70. self[field] = nil
  71. else
  72. self[field] = value
  73. end
  74. value
  75. }
  76. end
  77. TEMPLATE_VALUES = %w(has_option_icon
  78. order_results_by
  79. prevent_anonymous
  80. vote_method
  81. material_icon
  82. require_all_choices
  83. validate_minimum_stance_choices
  84. validate_maximum_stance_choices
  85. validate_min_score
  86. validate_max_score
  87. has_options
  88. validate_dots_per_person).freeze
  89. TEMPLATE_VALUES.each do |field|
  90. define_method field, -> { AppConfig.poll_types.dig(self.poll_type, field) }
  91. end
  92. def poll_template
  93. return PollTemplate.find_by(id: poll_template_id) if poll_template_id
  94. return PollTemplateService.default_templates.find {|pt| pt.key == poll_template_key } if poll_template_key
  95. return nil
  96. end
  97. def create_missing_created_event!
  98. self.events.create(
  99. kind: created_event_kind,
  100. user_id: author_id,
  101. created_at: created_at,
  102. discussion_id: discussion_id)
  103. end
  104. def minimum_stance_choices
  105. if require_all_choices
  106. poll.poll_options.length
  107. else
  108. self[:minimum_stance_choices] ||
  109. self[:custom_fields][:minimum_stance_choices] ||
  110. AppConfig.poll_types.dig(self.poll_type, 'defaults', 'minimum_stance_choices') ||
  111. 0
  112. end
  113. end
  114. def maximum_stance_choices
  115. self[:maximum_stance_choices] ||
  116. self[:custom_fields][:maximum_stance_choices] ||
  117. AppConfig.poll_types.dig(self.poll_type, 'defaults', 'maximum_stance_choices') ||
  118. poll.poll_options.length
  119. end
  120. include Translatable
  121. is_translatable on: [:title, :details]
  122. is_mentionable on: :details
  123. belongs_to :author, class_name: "User"
  124. has_many :outcomes, dependent: :destroy
  125. has_one :current_outcome, -> { where(latest: true) }, class_name: 'Outcome'
  126. belongs_to :discussion
  127. belongs_to :group, class_name: "Group"
  128. enum notify_on_closing_soon: {nobody: 0, author: 1, undecided_voters: 2, voters: 3}
  129. enum hide_results: {off: 0, until_vote: 1, until_closed: 2}
  130. enum stance_reason_required: {disabled: 0, optional: 1, required: 2}
  131. has_many :stances, dependent: :destroy
  132. has_many :stance_choices, through: :stances
  133. has_many :voters, -> { merge(Stance.latest) }, through: :stances, source: :participant
  134. has_many :admin_voters, -> { merge(Stance.latest.admin) }, through: :stances, source: :participant
  135. has_many :undecided_voters, -> { merge(Stance.latest.undecided) }, through: :stances, source: :participant
  136. has_many :decided_voters, -> { merge(Stance.latest.decided) }, through: :stances, source: :participant
  137. has_many :poll_options, -> { order('priority') }, dependent: :destroy, autosave: true
  138. accepts_nested_attributes_for :poll_options, allow_destroy: true
  139. has_many :documents, as: :model, dependent: :destroy
  140. scope :dangling, -> { joins('left join groups g on polls.group_id = g.id').where('group_id is not null and g.id is null') }
  141. scope :active, -> { kept.where('polls.closed_at': nil) }
  142. scope :template, -> { kept.where('polls.template': true) }
  143. scope :closed, -> { kept.where("polls.closed_at IS NOT NULL") }
  144. scope :recent, -> { kept.where("polls.closed_at IS NULL or polls.closed_at > ?", 7.days.ago) }
  145. scope :search_for, ->(fragment) { kept.where("polls.title ilike :fragment", fragment: "%#{fragment}%") }
  146. scope :lapsed_but_not_closed, -> { active.where("polls.closing_at < ?", Time.now) }
  147. scope :active_or_closed_after, ->(since) { kept.where("polls.closed_at IS NULL OR polls.closed_at > ?", since) }
  148. scope :in_organisation, -> (group) { kept.where(group_id: group.id_and_subgroup_ids) }
  149. scope :closing_soon_not_published, ->(timeframe, recency_threshold = 24.hours.ago) do
  150. active
  151. .distinct
  152. .where(closing_at: timeframe)
  153. .where("NOT EXISTS (SELECT 1 FROM events
  154. WHERE events.created_at > ? AND
  155. events.eventable_id = polls.id AND
  156. events.eventable_type = 'Poll' AND
  157. events.kind = 'poll_closing_soon')", recency_threshold)
  158. end
  159. validates :poll_type, inclusion: { in: AppConfig.poll_types.keys }
  160. validates :details, length: {maximum: Rails.application.secrets.max_message_length }
  161. before_save :clamp_minimum_stance_choices
  162. validate :closes_in_future
  163. validate :discussion_group_is_poll_group
  164. validate :cannot_deanonymize
  165. validate :cannot_reveal_results_early
  166. validate :title_if_not_discarded
  167. alias_method :user, :author
  168. has_paper_trail only: [
  169. :author_id,
  170. :title,
  171. :details,
  172. :details_format,
  173. :closing_at,
  174. :closed_at,
  175. :group_id,
  176. :discussion_id,
  177. :anonymous,
  178. :discarded_at,
  179. :discarded_by,
  180. :voter_can_add_options,
  181. :specified_voters_only,
  182. :stance_reason_required,
  183. :tags,
  184. :notify_on_closing_soon,
  185. :poll_option_names,
  186. :hide_results]
  187. update_counter_cache :group, :polls_count
  188. update_counter_cache :group, :closed_polls_count
  189. update_counter_cache :discussion, :closed_polls_count
  190. update_counter_cache :discussion, :anonymous_polls_count
  191. delegate :locale, to: :author
  192. delegate :name, to: :author, prefix: true
  193. def has_score_icons
  194. vote_method == "time_poll"
  195. end
  196. def has_variable_score
  197. !(min_score == max_score)
  198. end
  199. def is_single_choice?
  200. minimum_stance_choices == 1 && maximum_stance_choices == 1
  201. end
  202. def results_include_undecided
  203. poll_type != "meeting"
  204. end
  205. def dates_as_options
  206. poll_option_name_format == 'iso8601'
  207. end
  208. def chart_column
  209. case poll_type
  210. when 'count' then (agree_target ? 'target_percent' : 'voter_percent')
  211. when 'check', 'proposal' then 'score_percent'
  212. else
  213. 'max_score_percent'
  214. end
  215. end
  216. def can_respond_maybe
  217. self[:custom_fields].fetch('can_respond_maybe', false)
  218. end
  219. def result_columns
  220. case poll_type
  221. when 'proposal'
  222. %w[chart name score_percent voter_count voters]
  223. when 'check'
  224. %w[chart name voter_percent voter_count voters]
  225. when 'count'
  226. if agree_target
  227. %w[chart name target_percent voter_count voters]
  228. else
  229. %w[chart name voter_count voters]
  230. end
  231. when 'ranked_choice'
  232. %w[chart name rank score_percent score average]
  233. when 'dot_vote'
  234. %w[chart name score_percent score average voter_count]
  235. when 'score'
  236. %w[chart name score average voter_count]
  237. when 'poll'
  238. %w[chart name score_percent voter_count voters]
  239. when 'meeting'
  240. %w[chart name score voters]
  241. else
  242. []
  243. end
  244. end
  245. def results
  246. PollService.calculate_results(self, self.poll_options)
  247. end
  248. def user_id
  249. author_id
  250. end
  251. def existing_member_ids
  252. voter_ids
  253. end
  254. def decided_voters_count
  255. voters_count - undecided_voters_count
  256. end
  257. def cast_stances_pct
  258. return 0 if voters_count == 0
  259. ((decided_voters_count.to_f / voters_count) * 100).to_i
  260. end
  261. def undecided_voters
  262. anonymous? ? User.none : super
  263. end
  264. def decided_voters
  265. anonymous? ? User.none : super
  266. end
  267. def unmasked_voters
  268. User.where(id: stances.latest.pluck(:participant_id))
  269. end
  270. def unmasked_undecided_voters
  271. User.where(id: stances.latest.undecided.pluck(:participant_id))
  272. end
  273. def unmasked_decided_voters
  274. User.where(id: stances.latest.decided.pluck(:participant_id))
  275. end
  276. def body
  277. details
  278. end
  279. def body=(val)
  280. self.details = val
  281. end
  282. def body_format
  283. details_format
  284. end
  285. def time_zone
  286. custom_fields.fetch('time_zone', author.time_zone)
  287. end
  288. def parent_event
  289. if discussion
  290. discussion.created_event
  291. else
  292. nil
  293. end
  294. end
  295. def group
  296. super || NullGroup.new
  297. end
  298. def show_results?(voted: false)
  299. !! case hide_results
  300. when 'until_closed'
  301. closed_at
  302. when 'until_vote'
  303. closed_at || voted
  304. else
  305. true
  306. end
  307. end
  308. # this should not be run on anonymous polls
  309. def reset_latest_stances!
  310. self.transaction do
  311. self.stances.update_all(latest: false)
  312. Stance.where("id IN
  313. (SELECT DISTINCT ON (participant_id) id
  314. FROM stances
  315. WHERE poll_id = #{id}
  316. ORDER BY participant_id, created_at DESC)").update_all(latest: true)
  317. end
  318. end
  319. def total_score
  320. stance_counts.sum
  321. end
  322. def update_counts!
  323. poll_options.reload.each(&:update_counts!)
  324. update_columns(
  325. stance_counts: poll_options.map(&:total_score), # should rename to option scores
  326. voters_count: stances.latest.count, # should rename to stances_count
  327. undecided_voters_count: stances.latest.undecided.count,
  328. versions_count: versions.count
  329. )
  330. end
  331. # people who administer the poll (not necessarily vote)
  332. def admins
  333. raise "poll.admins only makes sense for persisted polls" if self.new_record?
  334. User.active.
  335. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.discussion_id || 0} AND dr.user_id = users.id").
  336. joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
  337. joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{self.id || 0}").
  338. joins("LEFT OUTER JOIN polls p ON p.author_id = users.id AND p.id = #{self.id || 0}").
  339. where("(m.id IS NOT NULL AND m.revoked_at IS NULL AND m.admin = TRUE) OR /* group admin */
  340. (p.author_id = users.id AND p.group_id IS NOT NULL AND m.id IS NOT NULL) OR /* poll author and group member */
  341. (p.author_id = users.id AND p.group_id IS NULL) OR /* poll author and no group */
  342. (p.author_id = users.id AND dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR /* poll author and discussion guest */
  343. (dr.id IS NOT NULL AND m.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.admin = TRUE) OR /* discussion admin, group member */
  344. (dr.id IS NOT NULL AND m.id IS NULL AND dr.revoked_at IS NULL AND dr.admin = TRUE AND dr.guest = TRUE) OR /* discussion guest admin, not group member */
  345. (s.id IS NOT NULL AND m.id IS NOT NULL AND s.revoked_at IS NULL AND latest = TRUE AND s.admin = TRUE) OR /* poll admin, group member */
  346. (s.id IS NOT NULL AND m.id IS NULL AND s.revoked_at IS NULL AND latest = TRUE AND s.admin = TRUE AND s.guest = TRUE /* poll admin guest */)")
  347. end
  348. # people who can read the poll, not necessarily vote
  349. def members
  350. User.active.
  351. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.discussion_id || 0} AND dr.user_id = users.id").
  352. joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
  353. joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{self.id || 0}").
  354. where("(dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR
  355. (m.id IS NOT NULL AND m.revoked_at IS NULL) OR
  356. (s.id IS NOT NULL AND s.revoked_at IS NULL AND s.guest = TRUE AND latest = TRUE)")
  357. end
  358. def add_guest!(user, author)
  359. stances.create!(participant_id: user.id, inviter: author, guest: true, volume: DiscussionReader.volumes[:normal])
  360. end
  361. def add_admin!(user, author)
  362. stances.create!(participant_id: user.id, inviter: author, volume: DiscussionReader.volumes[:normal], admin: true)
  363. end
  364. def active?
  365. (closing_at && closing_at > Time.now) && !closed_at
  366. end
  367. def wip?
  368. closing_at.nil?
  369. end
  370. def closed?
  371. !!closed_at
  372. end
  373. def poll_option_names
  374. poll_options.map(&:name)
  375. end
  376. def poll_option_names=(names)
  377. names = Array(names)
  378. existing = Array(poll_options.pluck(:name))
  379. names = names.sort if poll_type == 'meeting'
  380. names.each_with_index do |name, priority|
  381. option = poll_options.find_or_initialize_by(name: name)
  382. option.priority = priority
  383. os = AppConfig.poll_types.dig(self.poll_type, 'common_poll_options') || []
  384. if params = os.find {|o| o['key'] == name }
  385. option.name = I18n.t(params['name_i18n'])
  386. option.icon = params['icon']
  387. option.meaning = I18n.t(params['meaning_i18n'])
  388. option.prompt = I18n.t(params['prompt_i18n'])
  389. end
  390. end
  391. removed = (existing - names)
  392. poll_options.each {|option| option.mark_for_destruction if removed.include?(option.name) }
  393. names
  394. end
  395. alias options= poll_option_names=
  396. alias options poll_option_names
  397. def is_new_version?
  398. !self.poll_options.map(&:persisted?).all? ||
  399. (['title', 'details', 'closing_at'] & self.changes.keys).any?
  400. end
  401. def discussion_id=(discussion_id)
  402. super.tap { self.group_id = self.discussion&.group_id }
  403. end
  404. def discussion=(discussion)
  405. super.tap { self.group_id = self.discussion&.group_id }
  406. end
  407. def prioritise_poll_options!
  408. if self.poll_type == 'meeting'
  409. self.poll_options.sort {|a,b| a.name <=> b.name }.each_with_index {|o, i| o.priority = i }
  410. end
  411. end
  412. private
  413. def title_if_not_discarded
  414. if !discarded_at && title.to_s.empty?
  415. errors.add(:title, I18n.t(:"activerecord.errors.messages.blank"))
  416. end
  417. end
  418. def cannot_deanonymize
  419. if anonymous_changed? && anonymous_was == true
  420. errors.add :anonymous, :cannot_deanonymize
  421. end
  422. end
  423. def cannot_reveal_results_early
  424. if hide_results_changed? && (hide_results_was == 'until_closed')
  425. errors.add :hide_results, :cannot_show_results_early
  426. end
  427. end
  428. def closes_in_future
  429. return if closed_at
  430. return if closing_at.nil?
  431. return if closing_at > Time.zone.now
  432. errors.add(:closing_at, I18n.t(:"poll.error.must_be_in_the_future"))
  433. end
  434. def discussion_group_is_poll_group
  435. return if poll.group.nil?
  436. return if poll.discussion.nil?
  437. poll.group_id = poll.discussion.group_id if poll.group_id.nil? && poll.discussion.group_id
  438. return if poll.discussion.group_id == poll.group_id
  439. self.errors.add(:group, 'Poll group is not discussion group')
  440. end
  441. def clamp_minimum_stance_choices
  442. return if minimum_stance_choices.nil?
  443. if minimum_stance_choices > poll_options.length
  444. self.minimum_stance_choices = poll_options.length
  445. end
  446. end
  447. end

app/models/poll_option.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. 1 class PollOption < ApplicationRecord
  2. 1 include Translatable
  3. 1 belongs_to :poll
  4. 1 validates :name, presence: true
  5. 1 has_many :stance_choices, dependent: :destroy
  6. 9619 has_many :stances, -> { where("stances.revoked_at IS NULL") }, through: :stance_choices
  7. 1 is_translatable on: [:name, :meaning]
  8. 1 scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
  9. 1 def update_counts!
  10. 4809 update_columns(
  11. 8268 voter_scores: poll.anonymous ? {} : stance_choices.latest.where('stances.participant_id is not null').includes(:stance).map { |c| [c.stance.participant_id, c.score] }.to_h,
  12. total_score: stance_choices.latest.sum(:score),
  13. voter_count: stances.latest.count
  14. )
  15. end
  16. 1 def icon
  17. 8192 self[:icon] || {
  18. agree: 'agree',
  19. disagree: 'disagree',
  20. abstain: 'abstain',
  21. block: 'block',
  22. consent: 'agree',
  23. objection: 'disagree',
  24. yes: 'agree',
  25. no: 'disagree'
  26. }[name.to_sym]
  27. end
  28. 1 def color
  29. 9659 if poll.vote_method == 'show_thumbs'
  30. {
  31. 2560 'agree' => AppConfig.colors['proposal'][0],
  32. 'abstain' => AppConfig.colors['proposal'][1],
  33. 'disagree' => AppConfig.colors['proposal'][2],
  34. 'block' => AppConfig.colors['proposal'][3],
  35. }.fetch(icon, AppConfig.colors['proposal'][0])
  36. else
  37. 7099 AppConfig.colors.dig('poll', self.priority % AppConfig.colors.length)
  38. end
  39. end
  40. 1 def voter_ids
  41. # this is a hack, we both know this
  42. # some polls 0 is a vote, others it is not
  43. 1870 if poll.poll_type == 'meeting'
  44. 86 voter_scores.keys.map(&:to_i)
  45. else
  46. 2991 voter_scores.filter{|id, score| score != 0 }.keys.map(&:to_i)
  47. end
  48. end
  49. 1 def average_score
  50. 1870 return 0 if voter_count == 0
  51. 987 (total_score.to_f / voter_count.to_f)
  52. end
  53. end

app/models/poll_template.rb

58.06% lines covered

31 relevant lines. 18 lines covered and 13 lines missed.
    
  1. 1 class PollTemplate < ApplicationRecord
  2. 1 include Discard::Model
  3. 1 include HasRichText
  4. 1 include CustomCounterCache::Model
  5. 1 is_rich_text on: :details
  6. 1 belongs_to :author, class_name: "User"
  7. 1 belongs_to :group, class_name: "Group"
  8. 1 enum notify_on_closing_soon: {nobody: 0, author: 1, undecided_voters: 2, voters: 3}
  9. 1 enum hide_results: {off: 0, until_vote: 1, until_closed: 2}
  10. 1 enum stance_reason_required: {disabled: 0, optional: 1, required: 2}
  11. 1 update_counter_cache :group, :poll_templates_count
  12. 1 validates :poll_type, inclusion: { in: AppConfig.poll_types.keys }
  13. 1 validates :details, length: { maximum: Rails.application.secrets.max_message_length }
  14. 1 validates :process_name, presence: true
  15. 1 validates :process_subtitle, presence: true
  16. 1 validates :default_duration_in_days, presence: true
  17. 1 has_paper_trail only: [
  18. :poll_type,
  19. :process_name,
  20. :process_subtitle,
  21. :process_introduction,
  22. :process_introduction_format,
  23. :title,
  24. :details,
  25. :details_format,
  26. :group_id,
  27. :anonymous,
  28. :shuffle_options,
  29. :chart_type,
  30. :specified_voters_only,
  31. :stance_reason_required,
  32. :notify_on_closing_soon,
  33. :hide_results,
  34. :min_score,
  35. :max_score,
  36. :minimum_stance_choices,
  37. :maximum_stance_choices,
  38. :dots_per_person,
  39. :reason_prompt,
  40. :poll_options,
  41. :limit_reason_length,
  42. :default_duration_in_days,
  43. :meeting_duration,
  44. :can_respond_maybe,
  45. :tags,
  46. :discarded_at
  47. ]
  48. 1 def dump_i18n
  49. out = {}
  50. [
  51. :title,
  52. :title_placeholder,
  53. :process_name,
  54. :process_subtitle,
  55. :process_introduction,
  56. :details,
  57. :reason_prompt,
  58. ].map(&:to_s).each do |key|
  59. unless self.send(key) == AppConfig.poll_types.dig(self.poll_type, 'defaults', key)
  60. out[key] = self[key]
  61. end
  62. end
  63. tags.each do |tag|
  64. out[tag.underscore.gsub(" ", "_")] = tag
  65. end
  66. self.poll_options.each do |poll_option|
  67. option_name = poll_option.slice('name').values[0].parameterize(separator: '_').gsub('-', '_')
  68. poll_option.slice('name', 'meaning', 'prompt').each_pair do |key, value|
  69. if key == 'name'
  70. out[option_name] = value
  71. else
  72. out[option_name+"_"+key] = value
  73. end
  74. end
  75. end
  76. {process_name.underscore.gsub(" ", "_") => out}
  77. end
  78. end

app/models/reaction.rb

78.57% lines covered

14 relevant lines. 11 lines covered and 3 lines missed.
    
  1. 1 class Reaction < ApplicationRecord
  2. 1 belongs_to :reactable, polymorphic: true
  3. 1 belongs_to :user
  4. # TODO: ensure one reaction per reactable
  5. # validates_uniqueness_of :user_id, scope: :reactable
  6. 1 validates_presence_of :user, :reactable
  7. 1 delegate :group, to: :reactable, allow_nil: true
  8. 1 delegate :group_id, to: :reactable, allow_nil: true
  9. 1 delegate :members, to: :reactable, allow_nil: true
  10. 1 alias :author :user
  11. 1 def author_id
  12. 2 user_id
  13. end
  14. 1 def message_channel
  15. case reactable
  16. when Outcome, Stance, Poll then reactable.poll.message_channel
  17. when Comment, Discussion then reactable.discussion.message_channel
  18. end
  19. end
  20. end

app/models/received_email.rb

96.15% lines covered

52 relevant lines. 50 lines covered and 2 lines missed.
    
  1. 1 class ReceivedEmail < ApplicationRecord
  2. 1 has_many_attached :attachments
  3. 1 belongs_to :group
  4. 6 scope :unreleased, -> { where(released: false) }
  5. 1 scope :released, -> { where(released: true) }
  6. 1 def header(name)
  7. 1257 headers.find { |key, value| key.downcase == name.to_s.downcase }&.last
  8. end
  9. 1 def recipient_emails
  10. 73 String(header('to')).scan(AppConfig::EMAIL_REGEX).uniq
  11. end
  12. 1 def route_address
  13. 73 reply_hostnames = [ENV['REPLY_HOSTNAME'], ENV['OLD_REPLY_HOSTNAME']].compact
  14. 73 recipient_emails.find do |email|
  15. 73 reply_hostnames.include? email.split('@')[1].downcase
  16. end
  17. end
  18. 1 def route_path
  19. 46 route_address.split('@')[0]
  20. end
  21. 1 def sender_hostname
  22. 27 sender_email.split('@')[1]
  23. end
  24. 1 def sender_email
  25. 54 String(header('from')).scan(AppConfig::EMAIL_REGEX).uniq.first
  26. end
  27. 1 def sender_name
  28. 19 full_address = header('from').strip
  29. 19 name = full_address.split('<').first.strip.delete('"')
  30. 19 if name.present? && name != full_address
  31. 12 name
  32. else
  33. nil
  34. end
  35. end
  36. 1 def from
  37. 1 header('from').strip
  38. end
  39. 1 def sender_name_and_email
  40. 8 if sender_name
  41. 4 "\"#{sender_name}\" <#{sender_email}>"
  42. else
  43. 4 sender_email
  44. end
  45. end
  46. 1 def body_format
  47. 5 if body_html.present?
  48. 5 'html'
  49. else
  50. 'md'
  51. end
  52. end
  53. 1 def full_body
  54. 5 self.body_html.presence || self.body_text
  55. end
  56. 1 def reply_body
  57. 3 text = if body_html.present?
  58. 3 Premailer.new(body_html, line_length: 10000, with_html_string: true).to_plain_text
  59. else
  60. body_text
  61. end
  62. 3 ReceivedEmailService.extract_reply_body(text, sender_name)
  63. end
  64. 1 def subject
  65. 191 String(header('subject')).gsub(/^( *(re|fwd?)(:| ) *)+/i, '')
  66. end
  67. 1 def title
  68. 8 sender_name_and_email
  69. end
  70. 1 def is_addressed_to_loomio?
  71. 13 route_address.present?
  72. end
  73. 1 def is_auto_response?
  74. 13 return true if header('X-Autorespond')
  75. 13 return true if header('X-Precedence') == 'auto_reply'
  76. prefixes = [
  77. 13 'Auto:',
  78. 'Automatic reply',
  79. 'Autosvar',
  80. 'Automatisk svar',
  81. 'Automatisch antwoord',
  82. 'Abwesenheitsnotiz',
  83. 'Risposta Non al computer',
  84. 'Automatisch antwoord',
  85. 'Auto Response',
  86. 'Respuesta automática',
  87. 'Fuori sede',
  88. 'Out of Office',
  89. 'Frånvaro',
  90. 'Réponse automatique'
  91. ]
  92. 195 prefixes.any? { |prefix| subject.downcase.starts_with?(prefix.downcase) }
  93. end
  94. end

app/models/search_result.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. 1 class SearchResult
  2. 1 include ActiveModel::Model
  3. 1 include ActiveModel::Serialization
  4. 1 attr_accessor :id,
  5. :searchable_type,
  6. :searchable_id,
  7. :poll_title,
  8. :discussion_title,
  9. :discussion_key,
  10. :highlight,
  11. :poll_key,
  12. :poll_id,
  13. :sequence_id,
  14. :group_handle,
  15. :group_key,
  16. :group_id,
  17. :group_name,
  18. :author_name,
  19. :author_id,
  20. :authored_at,
  21. :tags
  22. 1 def poll
  23. Poll.find_by(id: poll_id)
  24. end
  25. 1 def author
  26. User.find_by(id: author_id)
  27. end
  28. end

app/models/site_settings.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. class SiteSettings
  2. def self.colors
  3. {
  4. primary: "#E3E4E6",
  5. agree: "#94D587",
  6. abstain: "#EEBC57",
  7. disagree: "#D1908F",
  8. block: "#D80D00"
  9. }.with_indifferent_access
  10. end
  11. end

app/models/stance.rb

89.04% lines covered

146 relevant lines. 130 lines covered and 16 lines missed.
    
  1. 1 class Stance < ApplicationRecord
  2. 1 include CustomCounterCache::Model
  3. 1 include HasMentions
  4. 1 include Reactable
  5. 1 include HasEvents
  6. 1 include HasCreatedEvent
  7. 1 include HasVolume
  8. 1 include Searchable
  9. 1 extend HasTokens
  10. 1 initialized_with_token :token
  11. 1 def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil, poll_id: nil)
  12. 634 content_str = "regexp_replace(CONCAT_WS(' ', stances.reason, users.name), E'<[^>]+>', '', 'gi')"
  13. 634 <<~SQL.squish
  14. INSERT INTO pg_search_documents (
  15. searchable_type,
  16. searchable_id,
  17. poll_id,
  18. group_id,
  19. discussion_id,
  20. author_id,
  21. authored_at,
  22. content,
  23. ts_content,
  24. created_at,
  25. updated_at)
  26. SELECT 'Stance' AS searchable_type,
  27. stances.id AS searchable_id,
  28. stances.poll_id AS poll_id,
  29. polls.group_id as group_id,
  30. polls.discussion_id AS discussion_id,
  31. stances.participant_id AS author_id,
  32. stances.cast_at AS authored_at,
  33. #{content_str} AS content,
  34. to_tsvector('simple', #{content_str}) as ts_content,
  35. now() AS created_at,
  36. now() AS updated_at
  37. FROM stances
  38. LEFT JOIN users ON users.id = stances.participant_id
  39. LEFT JOIN polls ON polls.id = stances.poll_id
  40. WHERE polls.discarded_at IS NULL
  41. AND stances.cast_at IS NOT null
  42. AND NOT (polls.anonymous = TRUE AND polls.closed_at IS NULL)
  43. AND NOT (polls.hide_results = 2 AND polls.closed_at IS NULL)
  44. #{id ? " AND stances.id = #{id.to_i} LIMIT 1" : ''}
  45. #{author_id ? " AND stances.participant_id = #{author_id.to_i}" : ''}
  46. #{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_i}" : ''}
  47. #{poll_id ? " AND stances.poll_id = #{poll_id.to_i}" : ''}
  48. SQL
  49. end
  50. 1 ORDER_SCOPES = ['newest_first', 'oldest_first', 'priority_first', 'priority_last']
  51. 1 include Translatable
  52. 1 is_translatable on: :reason
  53. 1 is_mentionable on: :reason
  54. 1 include HasRichText
  55. 1 is_rich_text on: :reason
  56. 1 belongs_to :poll, required: true
  57. 1 belongs_to :inviter, class_name: 'User'
  58. 1 has_many :stance_choices, dependent: :destroy
  59. 1 has_many :poll_options, through: :stance_choices
  60. 1 has_paper_trail only: [:reason, :option_scores, :revoked_at, :revoker_id, :inviter_id]
  61. 1 accepts_nested_attributes_for :stance_choices
  62. 1 belongs_to :participant, class_name: 'User', required: true
  63. 1 alias :user :participant
  64. 1 alias :author :participant
  65. 1 scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
  66. 21336 scope :latest, -> { where(latest: true, revoked_at: nil) }
  67. 1 scope :guests, -> { where(guest: true) }
  68. 1 scope :admins, -> { where(admin: true) }
  69. 1 scope :newest_first, -> { order("cast_at DESC NULLS LAST") }
  70. 1 scope :undecided_first, -> { order("cast_at DESC NULLS FIRST") }
  71. 1 scope :oldest_first, -> { order(created_at: :asc) }
  72. 1 scope :priority_first, -> { joins(:poll_options).order('poll_options.priority ASC') }
  73. 1 scope :priority_last, -> { joins(:poll_options).order('poll_options.priority DESC') }
  74. 291 scope :with_reason, -> { where("reason IS NOT NULL AND reason != '' AND reason != '<p></p>'") }
  75. 1 scope :in_organisation, ->(group) { joins(:poll).where("polls.group_id": group.id_and_subgroup_ids) }
  76. 47 scope :decided, -> { where("stances.cast_at IS NOT NULL") }
  77. 2323 scope :undecided, -> { where("stances.cast_at IS NULL") }
  78. 1270 scope :revoked, -> { where("revoked_at IS NOT NULL") }
  79. 21 scope :guests, -> { where("inviter_id is not null") }
  80. 3 scope :redeemable, -> { latest.guests.undecided.where('stances.accepted_at IS NULL') }
  81. 1 scope :redeemable_by, -> (user_id) {
  82. 2 redeemable.joins(:participant).where("stances.participant_id = ? or users.email_verified = false", user_id)
  83. }
  84. 1 validate :valid_minimum_stance_choices
  85. 1 validate :valid_maximum_stance_choices
  86. 1 validate :valid_dots_per_person
  87. 1 validate :valid_reason_length
  88. 1 validate :valid_reason_required
  89. 1 validate :valid_require_all_choices
  90. 1 %w(group mailer group_id discussion_id discussion members voters title tags).each do |message|
  91. 9 delegate(message, to: :poll)
  92. end
  93. 1 alias :author :participant
  94. 1 before_save :assign_option_scores
  95. 1 after_save :update_versions_count!
  96. 1 def build_replacement
  97. 53 Stance.new(
  98. poll_id: self.poll_id,
  99. participant_id: self.participant_id,
  100. inviter_id: self.inviter_id,
  101. reason_format: self.reason_format,
  102. latest: true
  103. )
  104. end
  105. 1 def create_missing_created_event!
  106. 310 self.events.create(
  107. kind: created_event_kind,
  108. 310 user_id: (poll.anonymous? ? nil: author_id),
  109. created_at: created_at,
  110. 310 discussion_id: (add_to_discussion? ? poll.discussion_id : nil)
  111. )
  112. end
  113. 1 def author_name
  114. 3 participant&.name
  115. end
  116. 1 def assign_option_scores
  117. 580 self.option_scores = build_option_scores
  118. end
  119. 1 def build_option_scores
  120. 1485 stance_choices.map { |sc| [sc.poll_option_id.to_s, sc.score] }.to_h
  121. end
  122. 1 def update_option_scores!
  123. 1 update_columns(option_scores: assign_option_scores)
  124. end
  125. 1 def update_versions_count!
  126. 578 update_columns(versions_count: versions.count)
  127. end
  128. 1 def author_id
  129. 245 participant_id
  130. end
  131. 1 def user_id
  132. 80 participant_id
  133. end
  134. 1 def locale
  135. author&.locale || group&.locale || poll.author.locale
  136. end
  137. 1 def add_to_discussion?
  138. 378 poll.discussion_id &&
  139. poll.hide_results != 'until_closed' &&
  140. !body_is_blank? &&
  141. !Event.where(eventable: self,
  142. discussion_id: poll.discussion_id,
  143. kind: ['stance_created', 'stance_updated']).exists?
  144. end
  145. 1 def body
  146. reason
  147. end
  148. 1 def body_format
  149. reason_format
  150. end
  151. 1 def parent_event
  152. 259 poll.created_event
  153. end
  154. 1 def discarded?
  155. 33 false
  156. end
  157. 1 def choice=(choice)
  158. 445 self.cast_at ||= Time.zone.now
  159. 445 if choice.kind_of?(Hash)
  160. 398 self.stance_choices_attributes = poll.poll_options.where(name: choice.keys).map do |option|
  161. 917 {poll_option_id: option.id,
  162. score: choice[option.name]}
  163. end
  164. else
  165. 47 options = poll.poll_options.where(name: choice)
  166. 47 self.stance_choices_attributes = options.map do |option|
  167. 48 {poll_option_id: option.id}
  168. end
  169. end
  170. end
  171. 1 def participant(bypass = false)
  172. 2552 super() if bypass
  173. 2552 (!participant_id || poll.anonymous?) ? AnonymousUser.new : super()
  174. end
  175. 1 def real_participant
  176. 104 User.find_by(id: participant_id)
  177. end
  178. 1 def score_for(option)
  179. option_scores[option.id] || 0
  180. end
  181. 1 private
  182. 1 def valid_min_score
  183. return if !cast_at
  184. return unless poll.validate_min_score
  185. return if (stance_choices.map(&:score).min || 0) >= poll.min_score
  186. errors.add(:stance_choices, "min_score validation failure")
  187. end
  188. 1 def valid_max_score
  189. return if !cast_at
  190. return unless poll.validate_max_score
  191. return if (stance_choices.map(&:score).max) <= poll.max_score
  192. errors.add(:stance_choices, "max_score validation failure")
  193. end
  194. 1 def valid_dots_per_person
  195. 2574 return if !cast_at
  196. 437 return unless poll.validate_dots_per_person
  197. 153 return if stance_choices.map(&:score).sum <= poll.dots_per_person.to_i
  198. errors.add(:dots_per_person, "Too many dots")
  199. end
  200. 1 def valid_minimum_stance_choices
  201. 2574 return if !cast_at
  202. 437 return unless poll.validate_minimum_stance_choices
  203. 278 return if stance_choices.length >= poll.minimum_stance_choices
  204. 5 errors.add(:stance_choices, "too few stance choices")
  205. end
  206. 1 def valid_maximum_stance_choices
  207. 2574 return if !cast_at
  208. 437 return unless poll.validate_maximum_stance_choices
  209. 178 return if stance_choices.length <= poll.maximum_stance_choices
  210. errors.add(:stance_choices, "too many stance choices")
  211. end
  212. 1 def valid_require_all_choices
  213. 2574 return if !cast_at
  214. 437 return unless poll.require_all_choices
  215. 112 return if poll.poll_options.length == 0
  216. 112 return if stance_choices.length == poll.poll_options.length
  217. 3 errors.add(:stance_choices, "require_all_stance_choices")
  218. end
  219. 1 def valid_reason_length
  220. 2574 return if !cast_at
  221. 437 return if !poll.limit_reason_length
  222. 437 return if reason_visible_text.length < 501
  223. 1 errors.add(:reason, I18n.t(:"poll_common.too_long"))
  224. end
  225. 1 def valid_reason_required
  226. 2574 return if !cast_at
  227. 437 return if poll.stance_reason_required != "required"
  228. return if reason_visible_text.length > 5
  229. errors.add(:reason, I18n.t(:"poll_common_form.stance_reason_is_required"))
  230. end
  231. end

app/models/stance_choice.rb

78.26% lines covered

23 relevant lines. 18 lines covered and 5 lines missed.
    
  1. 1 class StanceChoice < ApplicationRecord
  2. 1 belongs_to :poll_option
  3. 1 belongs_to :stance
  4. 1 has_one :poll, through: :poll_option
  5. 1 delegate :has_variable_score, to: :poll, allow_nil: true
  6. 1 validates_presence_of :poll_option
  7. 1 validate :total_score_is_valid
  8. 912 validates :score, numericality: { equal_to: 1 }, if: Proc.new { |sc| sc.stance && !sc.stance.cast_at && sc.poll && !sc.has_variable_score }
  9. 1 scope :dangling, -> { joins('left join stances on stances.id = stance_id').where('stances.id': nil) }
  10. 8340 scope :latest, -> { joins(:stance).where('stances.latest': true).where('stances.revoked_at': nil) }
  11. 1 scope :reasons_first, -> {
  12. joins(:stance).order(Arel.sql("CASE coalesce(stances.reason, '') WHEN '' THEN 1 ELSE 0 END"))
  13. .order(:created_at)
  14. }
  15. 1 def rank
  16. self.poll.minimum_stance_choices - self.score + 1 if poll.poll_type == 'ranked_choice'
  17. end
  18. 1 def rank_or_score
  19. rank || score
  20. end
  21. 1 private
  22. 1 def total_score_is_valid
  23. 911 return unless poll # when we are cloning records and poll is not saved yet
  24. 901 if poll.custom_fields['min_score'] && score < poll.custom_fields['min_score'].to_i
  25. errors.add(:score, "Score lower than permitted min")
  26. end
  27. 901 if poll.custom_fields['max_score'] && score > poll.custom_fields['max_score'].to_i
  28. errors.add(:score, "Score higher than permitted max")
  29. end
  30. end
  31. end

app/models/subscription.rb

0.0% lines covered

63 relevant lines. 0 lines covered and 63 lines missed.
    
  1. class Subscription < ApplicationRecord
  2. class MaxMembersExceeded < StandardError; end
  3. class NotActive < StandardError; end
  4. include SubscriptionConcern if Object.const_defined?('SubscriptionConcern')
  5. PAYMENT_METHODS = ["chargify", "manual", "barter", "paypal"]
  6. ACTIVE_STATES = %w[active on_hold pending]
  7. scope :dangling, -> { joins('LEFT JOIN groups ON subscriptions.id = groups.subscription_id').where('groups.id IS NULL') }
  8. scope :active, -> { where(state: ACTIVE_STATES).where("expires_at is null OR expires_at > ?", Time.current) }
  9. scope :expired, -> { where(state: ACTIVE_STATES).where("expires_at < ?", Time.current) }
  10. scope :canceled, -> { where(state: :canceled) }
  11. has_many :groups
  12. belongs_to :owner, class_name: 'User'
  13. attr_accessor :chargify_product_id
  14. has_paper_trail
  15. def self.for(group)
  16. parent = group.parent_or_self
  17. parent.subscription || begin
  18. parent.subscription = Subscription.new
  19. parent.save
  20. parent.subscription
  21. end
  22. end
  23. def can_invite()
  24. parent_group = parent_or_self
  25. subscription = Subscription.for(parent_group)
  26. subscription.max_members && parent_group.org_members_count >= subscription.max_members
  27. end
  28. def level
  29. SubscriptionService::PLANS[self.plan][:level]
  30. end
  31. def config
  32. SubscriptionService::PLANS[Subscription.last.plan.to_sym]
  33. end
  34. def is_active?
  35. ACTIVE_STATES.include?(state) && (self.expires_at.nil? || self.expires_at > Time.current)
  36. end
  37. def management_link
  38. (self.info || {})['chargify_management_link']
  39. end
  40. def self.ransackable_associations(auth_object = nil)
  41. ["groups", "owner", "versions"]
  42. end
  43. def self.ransackable_attributes(auth_object = nil)
  44. ["activated_at",
  45. "canceled_at",
  46. "chargify_subscription_id",
  47. "created_at",
  48. "expires_at",
  49. "id",
  50. "info",
  51. "max_members",
  52. "max_orgs",
  53. "max_threads",
  54. "members_count",
  55. "owner_id",
  56. "payment_method",
  57. "plan",
  58. "renewed_at",
  59. "renews_at",
  60. "state",
  61. "updated_at"]
  62. end
  63. end

app/models/tag.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. class Tag < ApplicationRecord
  2. COLORS = %w[#f44336
  3. #e91e63
  4. #9c27b0
  5. #673ab7
  6. #3f51b5
  7. #2196f3
  8. #03a9f4
  9. #00bcd4
  10. #009688
  11. #4caf50
  12. #8bc34a
  13. #ffeb3b
  14. #ffc107
  15. #ff9800
  16. #ff5722
  17. #795548
  18. #607d8b
  19. #9e9e9e]
  20. include Translatable
  21. is_translatable on: :name
  22. belongs_to :group
  23. validates :name, presence: true, uniqueness: { scope: :group }
  24. # validates :color, presence: true, format: /\A#([A-F0-9]{3}){1,2}\z/i
  25. before_validation :set_defaults
  26. private
  27. def set_defaults
  28. colors = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', '#ff5722', '#795548', '#607d8b', '#9e9e9e']
  29. self.color = colors.sample if color.blank?
  30. end
  31. end

app/models/tagging.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class Tagging < ActiveRecord::Base
  2. include CustomCounterCache::Model
  3. belongs_to :taggable, polymorphic: true
  4. belongs_to :tag
  5. update_counter_cache :tag, :taggings_count
  6. end

app/models/task.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Task < ApplicationRecord
  2. 1 include Discard::Model
  3. 1 belongs_to :record, polymorphic: true
  4. 1 belongs_to :author, class_name: 'User'
  5. 1 belongs_to :doer, class_name: 'User'
  6. 2 scope :not_done, -> { where(done: false) }
  7. 1 has_many :tasks_users
  8. 1 has_many :users, through: :tasks_users
  9. end

app/models/tasks_user.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class TasksUser < ApplicationRecord
  2. 1 belongs_to :task
  3. 1 belongs_to :user
  4. end

app/models/translation.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Translation < ApplicationRecord
  2. belongs_to :translatable, polymorphic: true
  3. scope :to_language, ->(language) { where(language: language) }
  4. validates_presence_of :translatable, :language
  5. # validates :fields, presence: true
  6. # TODO: Should probably move to a serializer
  7. def as_json
  8. { id: translatable_id }.merge(fields)
  9. end
  10. end

app/models/user.rb

0.0% lines covered

311 relevant lines. 0 lines covered and 311 lines missed.
    
  1. class User < ApplicationRecord
  2. include CustomCounterCache::Model
  3. include ReadableUnguessableUrls
  4. include MessageChannel
  5. include HasExperiences
  6. include HasAvatar
  7. include SelfReferencing
  8. include NoForbiddenEmails
  9. include CustomCounterCache::Model
  10. include HasRichText
  11. include LocalesHelper
  12. is_rich_text on: :short_bio
  13. extend HasTokens
  14. extend HasDefaults
  15. extend NoSpam
  16. no_spam_for :name, :email
  17. has_paper_trail only: [:name, :username, :email, :email_newsletter, :deactivated_at, :deactivator_id]
  18. MAX_AVATAR_IMAGE_SIZE_CONST = 100.megabytes
  19. devise :database_authenticatable, :recoverable, :registerable, :rememberable, :lockable, :trackable
  20. devise :pwned_password if Rails.env.production?
  21. attr_accessor :recaptcha
  22. attr_accessor :restricted
  23. attr_accessor :token
  24. attr_accessor :membership_token
  25. attr_accessor :group_token
  26. attr_accessor :discussion_reader_token
  27. attr_accessor :stance_token
  28. attr_accessor :legal_accepted
  29. attr_writer :has_password
  30. attr_accessor :require_valid_signup
  31. attr_accessor :require_recaptcha
  32. before_save :set_legal_accepted_at, if: :legal_accepted
  33. validates :email, presence: true, email: true, length: {maximum: 200}, if: -> { !bot }
  34. validates :name, presence: true, if: :require_valid_signup
  35. validates :legal_accepted, presence: true, if: :require_legal_accepted
  36. validate :validate_recaptcha, if: :require_recaptcha
  37. has_one_attached :uploaded_avatar
  38. validates_uniqueness_of :email, conditions: -> { where(email_verified: true) }, if: :email_verified?
  39. validates_uniqueness_of :username, if: :email
  40. before_validation :generate_username, if: :email
  41. validates_length_of :name, maximum: 100
  42. validates_length_of :username, maximum: 30
  43. validates_length_of :short_bio, maximum: 5000
  44. validates_format_of :username, with: /\A[a-z0-9]*\z/, message: I18n.t(:'user.error.username_must_be_alphanumeric')
  45. validates_confirmation_of :password, if: :password_required?
  46. validates_length_of :password, minimum: 8, allow_nil: true
  47. has_many :admin_memberships,
  48. -> { where('memberships.admin': true, revoked_at: nil) },
  49. class_name: 'Membership'
  50. has_many :memberships, -> { active }, dependent: :destroy
  51. has_many :all_memberships, dependent: :destroy, class_name: "Membership"
  52. has_many :adminable_groups,
  53. -> { where(archived_at: nil) },
  54. through: :admin_memberships,
  55. class_name: 'Group',
  56. source: :group
  57. has_many :membership_requests,
  58. foreign_key: 'requestor_id',
  59. dependent: :destroy
  60. has_many :groups,
  61. -> { where archived_at: nil },
  62. through: :memberships
  63. has_many :discussions, through: :groups
  64. has_many :authored_discussions, class_name: 'Discussion', foreign_key: 'author_id', dependent: :destroy
  65. has_many :authored_polls, class_name: 'Poll', foreign_key: :author_id, dependent: :destroy
  66. has_many :created_groups, class_name: 'Group', foreign_key: :creator_id, dependent: :destroy
  67. has_many :identities, class_name: "Identities::Base", dependent: :destroy
  68. has_many :reactions, dependent: :destroy
  69. has_many :stances, foreign_key: :participant_id, dependent: :destroy
  70. has_many :participated_polls, through: :stances, source: :poll
  71. has_many :group_polls, through: :groups, source: :polls
  72. has_many :discussion_readers, dependent: :destroy
  73. has_many :guest_discussion_readers, -> { DiscussionReader.active.guests }, class_name: 'DiscussionReader', dependent: :destroy
  74. has_many :guest_discussions, through: :guest_discussion_readers, source: :discussion
  75. has_many :guest_stances, -> { Stance.latest.guests }, class_name: 'Stance', dependent: :destroy, foreign_key: :participant_id
  76. has_many :guest_polls, through: :guest_stances, source: :poll
  77. has_many :notifications, dependent: :destroy
  78. has_many :comments, dependent: :destroy
  79. has_many :documents, foreign_key: :author_id, dependent: :destroy
  80. has_many :login_tokens, dependent: :destroy
  81. has_many :events, dependent: :destroy
  82. has_many :tags, through: :groups
  83. before_save :set_avatar_initials
  84. initialized_with_token :unsubscribe_token, -> { Devise.friendly_token }
  85. initialized_with_token :email_api_key, -> { SecureRandom.hex(16) }
  86. initialized_with_token :api_key, -> { SecureRandom.hex(16) }
  87. enum default_membership_volume: [:mute, :quiet, :normal, :loud]
  88. scope :active, -> { where(deactivated_at: nil) }
  89. scope :deactivated, -> { where("deactivated_at IS NOT NULL") }
  90. scope :sorted_by_name, -> { order("lower(name)") }
  91. scope :admins, -> { where(is_admin: true) }
  92. scope :coordinators, -> { joins(:memberships).where('memberships.admin = ?', true).group('users.id') }
  93. scope :verified, -> { where(email_verified: true) }
  94. scope :unverified, -> { where(email_verified: false) }
  95. scope :search_for, -> (q) { where("users.name ilike :first OR users.name ilike :other OR users.username ilike :first OR users.email ilike :first", first: "#{q}%", other: "% #{q}%") }
  96. scope :visible_by, -> (user) { distinct.active.verified.joins(:memberships).where("memberships.group_id": user.group_ids).where.not(id: user.id) }
  97. scope :humans, -> { where(bot: false) }
  98. scope :bots, -> { where(bot: true) }
  99. scope :mention_search, -> (user, model, query) do
  100. return self.none unless model.present?
  101. ids = []
  102. if model.group_id
  103. ids += Membership.active.where(group_id: model.group_id).pluck(:user_id) if model.group_id
  104. end
  105. if model.discussion_id
  106. ids += DiscussionReader.active.guests.where(discussion_id: model.discussion_id).pluck(:user_id)
  107. end
  108. if model.poll_id
  109. ids += Stance.latest.guests.where(poll_id: model.poll_id).pluck(:participant_id)
  110. end
  111. if model.respond_to?(:poll_ids) and model.poll_ids.any?
  112. ids += Stance.latest.guests.where(poll_id: model.poll_ids).pluck(:participant_id)
  113. end
  114. active.search_for(query).where(id: ids)
  115. end
  116. scope :email_when_proposal_closing_soon, -> { active.where(email_when_proposal_closing_soon: true) }
  117. scope :email_proposal_closing_soon_for, -> (group) {
  118. email_when_proposal_closing_soon
  119. .joins(:memberships)
  120. .where('memberships.group_id': group.id)
  121. }
  122. def default_format
  123. if experiences['html-editor.uses-markdown']
  124. 'md'
  125. else
  126. 'html'
  127. end
  128. end
  129. def date_time_pref
  130. self[:date_time_pref] || 'day_abbr'
  131. end
  132. def author
  133. self
  134. end
  135. def is_paying?
  136. group_ids = self.group_ids.concat(self.groups.pluck(:parent_id).compact).uniq
  137. Group.where(id: group_ids).where(parent_id: nil).joins(:subscription).where.not('subscriptions.plan': 'trial').exists?
  138. end
  139. def is_paying
  140. is_paying?
  141. end
  142. def invitations_rate_limit
  143. if user.is_paying?
  144. ENV.fetch('PAID_INVITATIONS_RATE_LIMIT', 50000)
  145. else
  146. ENV.fetch('TRIAL_INVITATIONS_RATE_LIMIT', 500)
  147. end.to_i
  148. end
  149. def browseable_group_ids
  150. Group.where(
  151. "id in (:group_ids) OR
  152. (parent_id in (:group_ids) AND is_visible_to_parent_members = TRUE)",
  153. group_ids: self.group_ids).pluck(:id)
  154. end
  155. def set_legal_accepted_at
  156. self.legal_accepted_at = Time.now
  157. end
  158. def require_legal_accepted
  159. self.require_valid_signup && ENV['TERMS_URL']
  160. end
  161. def self.email_status_for(email)
  162. find_by(email: email)&.email_status || :unused
  163. end
  164. def self.find_for_database_authentication(warden_conditions)
  165. super(warden_conditions.merge(email_verified: true))
  166. end
  167. define_counter_cache(:memberships_count) {|user| user.memberships.count }
  168. def associate_with_identity(identity)
  169. if existing = identities.find_by(user: self, uid: identity.uid, identity_type: identity.identity_type)
  170. existing.update(access_token: identity.access_token)
  171. identity = existing
  172. else
  173. identities.push(identity)
  174. end
  175. update(name: identity.name) if self.name.nil?
  176. identity.assign_logo! unless self.avatar_url
  177. self
  178. end
  179. def identity_for(type)
  180. identities.find_by(identity_type: type)
  181. end
  182. def first_name
  183. name.split(' ').first
  184. end
  185. def last_name
  186. name.split(' ').drop(1).join(' ')
  187. end
  188. def remember_me
  189. true
  190. end
  191. def is_logged_in?
  192. true
  193. end
  194. def has_password
  195. self.encrypted_password.present?
  196. end
  197. def email_status
  198. if deactivated_at.present? then :inactive else :active end
  199. end
  200. def name_and_email
  201. "\"#{name}\" <#{email}>"
  202. end
  203. # Provide can? and cannot? as methods for checking permissions
  204. def ability
  205. @ability ||= ::Ability::Base.new(self)
  206. end
  207. delegate :can?, :cannot?, :to => :ability
  208. def is_member_of?(group)
  209. !!memberships.find_by(group_id: group&.id)
  210. end
  211. def is_admin_of?(group)
  212. !!memberships.find_by(group_id: group&.id, admin: true)
  213. end
  214. def first_name
  215. self.name.to_s.split(' ').first
  216. end
  217. def time_zone
  218. return 'UTC' if self[:time_zone] == "Etc/Unknown"
  219. self[:time_zone] || 'UTC'
  220. end
  221. def self.helper_bot
  222. verified.find_by(email: BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS) ||
  223. create!(email: BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS,
  224. name: 'Loomio Helper Bot',
  225. password: SecureRandom.hex(20),
  226. email_verified: true,
  227. bot: true,
  228. avatar_kind: :gravatar)
  229. end
  230. def name
  231. if deactivated_at && self[:name].nil?
  232. I18n.t('profile_page.deleted_account')
  233. else
  234. self[:name]
  235. end
  236. end
  237. def name_or_username
  238. self[:name] || self[:username]
  239. end
  240. # http://stackoverflow.com/questions/5140643/how-to-soft-delete-user-with-devise/8107966#8107966
  241. def active_for_authentication?
  242. super && !deactivated_at
  243. end
  244. def locale
  245. first_supported_locale([selected_locale, detected_locale].compact)
  246. end
  247. def update_detected_locale(locale)
  248. self.update_attribute(:detected_locale, locale) if self.detected_locale&.to_sym != locale.to_sym
  249. end
  250. def generate_username
  251. self.username ||= ::UsernameGenerator.new(self).generate
  252. end
  253. def send_devise_notification(notification, *args)
  254. I18n.with_locale(locale) { devise_mailer.send(notification, self, *args).deliver_now }
  255. end
  256. def self.ransackable_attributes(auth_object = nil)
  257. [
  258. "avatar_initials",
  259. "avatar_kind",
  260. "city",
  261. "content_locale",
  262. "country",
  263. "created_at",
  264. "current_sign_in_at",
  265. "current_sign_in_ip",
  266. "date_time_pref",
  267. "deactivated_at",
  268. "detected_locale",
  269. "email",
  270. "email_catch_up",
  271. "email_catch_up_day",
  272. "email_newsletter",
  273. "email_on_participation",
  274. "email_verified",
  275. "email_when_mentioned",
  276. "email_when_proposal_closing_soon",
  277. "id",
  278. "is_admin",
  279. "key",
  280. "last_seen_at",
  281. "last_sign_in_at",
  282. "last_sign_in_ip",
  283. "legal_accepted_at",
  284. "link_previews",
  285. "location",
  286. "locked_at",
  287. "memberships_count",
  288. "name",
  289. "region",
  290. "secret_token",
  291. "selected_locale",
  292. "short_bio",
  293. "short_bio_format",
  294. "sign_in_count",
  295. "time_zone",
  296. "updated_at",
  297. "uploaded_avatar_content_type",
  298. "uploaded_avatar_file_name",
  299. "uploaded_avatar_file_size",
  300. "uploaded_avatar_updated_at",
  301. "username"]
  302. end
  303. protected
  304. def password_required?
  305. !password.nil? || !password_confirmation.nil?
  306. end
  307. private
  308. def validate_recaptcha
  309. return unless ENV['RECAPTCHA_APP_KEY']
  310. return if Clients::Recaptcha.instance.validate(self.recaptcha)
  311. # Sentry.capture_message("recaptcha failed", extra: {email: email})
  312. self.errors.add(:recaptcha, I18n.t(:"user.error.recaptcha"))
  313. end
  314. end

app/queries/attachment_query.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class AttachmentQuery
  2. 1 def self.find(group_ids, query, limit, offset)
  3. 2 ids = []
  4. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  5. joins("LEFT OUTER JOIN groups ON active_storage_attachments.record_type = 'Group' AND active_storage_attachments.record_id = groups.id").
  6. where('groups.id IN (:group_ids)', group_ids: group_ids).
  7. where('active_storage_attachments.name': :files).
  8. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  9. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  10. joins("LEFT OUTER JOIN comments ON active_storage_attachments.record_type = 'Comment' AND active_storage_attachments.record_id = comments.id").
  11. joins("LEFT OUTER JOIN discussions comments_discussions ON comments_discussions.id = comments.discussion_id").
  12. where('comments_discussions.group_id IN (:group_ids) AND comments_discussions.discarded_at IS NULL AND comments.discarded_at IS NULL', group_ids: group_ids).
  13. where('active_storage_attachments.name': :files).
  14. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  15. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  16. joins("LEFT OUTER JOIN outcomes ON active_storage_attachments.record_type = 'Outcome' AND active_storage_attachments.record_id = outcomes.id").
  17. joins("LEFT OUTER JOIN polls outcomes_polls ON outcomes_polls.id = outcomes.poll_id").
  18. where('outcomes_polls.group_id IN (:group_ids) AND outcomes_polls.discarded_at IS NULL', group_ids: group_ids).
  19. where('active_storage_attachments.name': :files).
  20. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  21. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  22. joins("LEFT OUTER JOIN stances ON active_storage_attachments.record_type = 'Stance' AND active_storage_attachments.record_id = stances.id").
  23. joins("LEFT OUTER JOIN polls stances_polls ON stances_polls.id = stances.id").
  24. where('stances_polls.group_id IN (:group_ids) AND stances_polls.discarded_at IS NULL AND stances.revoked_at IS NULL', group_ids: group_ids).
  25. where('active_storage_attachments.name': :files).
  26. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  27. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  28. joins("LEFT OUTER JOIN discussions ON active_storage_attachments.record_type = 'Discussion' AND discussions.id = active_storage_attachments.record_id").
  29. where('discussions.group_id IN (:group_ids) AND discussions.discarded_at IS NULL', group_ids: group_ids).
  30. where('active_storage_attachments.name': :files).
  31. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  32. 2 ids.concat ActiveStorage::Attachment.joins(:blob).
  33. joins("LEFT OUTER JOIN polls ON active_storage_attachments.record_type = 'Poll' AND polls.id = active_storage_attachments.record_id").
  34. where('polls.group_id IN (:group_ids) AND polls.discarded_at IS NULL', group_ids: group_ids).
  35. where('active_storage_attachments.name': :files).
  36. where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
  37. 2 ActiveStorage::Attachment.joins(:blob).where(id: ids).order('id desc')
  38. end
  39. end

app/queries/contactable_query.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 class ContactableQuery
  2. 1 def self.contactable(user:, actor:)
  3. # the users have a group (membership or membership request) in common
  4. # the users have a discussion or poll in common
  5. # membership, membership_request, discussion or poll in common.
  6. 5 (group_ids(user) & group_ids(actor)).any? ||
  7. 3 (discussion_ids(user) & discussion_ids(actor)).any? ||
  8. 2 (poll_ids(user) & poll_ids(actor)).any?
  9. end
  10. 1 private
  11. 1 def self.group_ids(user)
  12. 10 %w[all_memberships
  13. membership_requests
  14. discussions
  15. group_polls
  16. guest_discussions
  17. participated_polls].map do |relation|
  18. 60 user.send(relation).pluck(:group_id)
  19. end.flatten.uniq
  20. end
  21. 1 def self.discussion_ids(user)
  22. 6 %w[discussions guest_discussions].map do |relation|
  23. 12 user.send(relation).pluck(:id)
  24. end.flatten.uniq
  25. end
  26. 1 def self.poll_ids(user)
  27. 4 %w[group_polls participated_polls].map do |relation|
  28. 8 user.send(relation).pluck(:id)
  29. end.flatten.uniq
  30. end
  31. end

app/queries/discussion_query.rb

86.96% lines covered

23 relevant lines. 20 lines covered and 3 lines missed.
    
  1. 1 class DiscussionQuery
  2. 1 def self.start
  3. 158 Discussion.
  4. kept.
  5. joins('LEFT OUTER JOIN groups ON discussions.group_id = groups.id').
  6. where('groups.archived_at IS NULL').
  7. includes(:author, :group)
  8. end
  9. 1 def self.dashboard(chain: start, user: )
  10. 4 chain = chain.where("discussions.group_id IN (:group_ids) OR discussions.id IN (:discussion_ids)",
  11. group_ids: user.group_ids, discussion_ids: user.guest_discussion_ids)
  12. end
  13. 1 def self.inbox(chain: start, user: )
  14. 4 chain.joins("LEFT OUTER JOIN discussion_readers dr ON discussions.id = dr.discussion_id AND dr.user_id = #{user.id}").
  15. where("discussions.group_id IN (:group_ids) OR discussions.id IN (:discussion_ids)", group_ids: user.group_ids, discussion_ids: user.guest_discussion_ids).
  16. where('dr.dismissed_at IS NULL OR (dr.dismissed_at < discussions.last_activity_at)').
  17. where('dr.last_read_at IS NULL OR (dr.last_read_at < discussions.last_activity_at)')
  18. end
  19. 1 def self.visible_to(chain: start,
  20. user: LoggedOutUser.new,
  21. group_ids: [],
  22. discussion_ids: [],
  23. tags: [],
  24. or_public: true,
  25. or_subgroups: true,
  26. only_direct: false,
  27. only_unread: false)
  28. 150 if user.discussion_reader_token
  29. or_discussion_reader_token = "OR dr.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
  30. end
  31. 150 chain = chain.joins("LEFT OUTER JOIN discussion_readers dr
  32. ON dr.discussion_id = discussions.id
  33. AND (dr.user_id = #{user.id || 0} #{or_discussion_reader_token})")
  34. .where("#{'(discussions.private = false) OR ' if or_public}
  35. (discussions.group_id in (:user_group_ids)) OR
  36. (dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE)
  37. #{'OR (groups.parent_members_can_see_discussions = TRUE AND groups.parent_id IN (:user_group_ids))' if or_subgroups}", user_group_ids: user.group_ids)
  38. 150 chain = chain.where("discussions.group_id IN (?)", group_ids) if Array(group_ids).any?
  39. 150 chain = chain.where("discussions.id IN (?)", discussion_ids) if Array(discussion_ids).any?
  40. 150 chain = chain.where("discussions.group_id IS NULL") if only_direct
  41. 150 chain = chain.where("tags @> ARRAY[?]::varchar[]", tags) if tags.any?
  42. 150 if only_unread
  43. 7 chain = chain.where('(dr.dismissed_at IS NULL) OR (dr.dismissed_at < discussions.last_activity_at)').
  44. where('dr.last_read_at IS NULL OR (dr.last_read_at < discussions.last_activity_at)')
  45. end
  46. 150 chain
  47. end
  48. 1 def self.filter(chain: , filter: )
  49. 7 case filter
  50. when 'show_closed', 'closed' then chain.is_closed
  51. when 'all' then chain
  52. 7 else chain.is_open
  53. end.order_by_latest_activity
  54. end
  55. end

app/queries/group_query.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class GroupQuery
  2. 1 def self.start
  3. 4 Group.includes(:subscription, :creator, :parent)
  4. end
  5. 1 def self.visible_to(user: LoggedOutUser.new, chain: start, show_public: false)
  6. 4 guest_discussion_group_ids = Discussion.where(id: user.guest_discussion_ids).pluck(:group_id)
  7. 4 group_ids = user.group_ids.concat(guest_discussion_group_ids)
  8. 4 chain.published.
  9. where("#{'is_visible_to_public = true OR ' if show_public}
  10. groups.id in (:group_ids) OR
  11. (parent_id in (:group_ids) AND is_visible_to_parent_members = TRUE)", group_ids: group_ids)
  12. end
  13. end

app/queries/membership_query.rb

80.0% lines covered

25 relevant lines. 20 lines covered and 5 lines missed.
    
  1. 1 class MembershipQuery
  2. 1 def self.start
  3. 6 Membership.includes(:group, :user, :inviter).joins(:group).joins(:user).active
  4. end
  5. 1 def self.visible_to(user: , chain: start)
  6. 6 chain.where("memberships.group_id IN (#{ids_or_null(user.group_ids)}) OR
  7. groups.parent_id IN (#{ids_or_null(user.adminable_group_ids)})")
  8. end
  9. 1 def self.search(chain: start, params:)
  10. 5 if group = Group.find_by(id: params[:group_id])
  11. 5 group_ids = case params[:subgroups]
  12. when 'mine', 'all'
  13. group.id_and_subgroup_ids
  14. else
  15. 5 [group.id]
  16. end
  17. 5 chain = chain.where(group_id: group_ids)
  18. end
  19. 5 if params[:user_ids]
  20. chain = chain.where('memberships.user_id': params[:user_ids])
  21. end
  22. 5 case params[:filter]
  23. when 'admin'
  24. chain = chain.admin
  25. when 'pending'
  26. chain = chain.pending
  27. when 'accepted'
  28. chain = chain.accepted
  29. end
  30. 5 query = params[:q].to_s
  31. 5 if query.length > 0
  32. 1 chain = chain.where("users.name ilike :first OR users.name ilike :last OR
  33. users.email ilike :first OR
  34. users.username ilike :first",
  35. first: "#{query}%", last: "% #{query}%")
  36. end
  37. 5 chain
  38. end
  39. 1 def self.ids_or_null(ids)
  40. 12 if ids.length == 0
  41. 4 'null'
  42. else
  43. 8 ids.join(',')
  44. end
  45. end
  46. end

app/queries/poll_query.rb

82.76% lines covered

29 relevant lines. 24 lines covered and 5 lines missed.
    
  1. 1 class PollQuery
  2. 1 def self.start
  3. 279 Poll.distinct.kept.includes(:poll_options, :group, :author)
  4. end
  5. 1 def self.visible_to(user: LoggedOutUser.new,
  6. chain: start,
  7. group_ids: [],
  8. show_public: false)
  9. 279 if user.discussion_reader_token
  10. or_discussion_reader_token = "OR dr.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
  11. end
  12. 279 if user.stance_token
  13. or_stance_token = "OR s.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
  14. end
  15. 279 chain = chain.where('polls.group_id IN (:group_ids)', group_ids: group_ids) if group_ids.any?
  16. 279 chain = chain.joins("LEFT OUTER JOIN discussions d on d.id = polls.discussion_id")
  17. 279 chain = chain.joins("LEFT OUTER JOIN memberships m ON m.group_id = polls.group_id AND m.user_id = #{user.id || 0}")
  18. .joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = polls.discussion_id AND (dr.user_id = #{user.id || 0} #{or_discussion_reader_token})")
  19. .joins("LEFT OUTER JOIN stances s ON s.poll_id = polls.id AND (s.participant_id = #{user.id || 0} #{or_stance_token})")
  20. .where("#{'d.private = false OR ' if show_public}
  21. polls.author_id = :user_id OR
  22. (m.id IS NOT NULL AND m.revoked_at IS NULL) OR
  23. (dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR
  24. (s.id IS NOT NULL AND s.revoked_at IS NULL AND s.guest = TRUE)", user_id: user.id)
  25. 279 chain
  26. end
  27. 1 def self.filter(chain: , params: )
  28. # how to do this....
  29. 8 if group = Group.find_by(key: params[:group_key])
  30. 1 group_ids = (params[:subgroups] == "none") ? [group.id] : group.id_and_subgroup_ids
  31. 1 chain = chain.where(group_id: group_ids)
  32. end
  33. 8 if discussion = Discussion.find_by(key: params[:discussion_key]) || Discussion.find_by(id: params[:discussion_id])
  34. 2 chain = chain.where(discussion_id: discussion.id)
  35. end
  36. 8 if (tags = (params[:tags] || '').split('|')).any?
  37. chain = chain.where.contains(tags: tags)
  38. end
  39. 8 if params[:status] == 'vote'
  40. voted_poll_ids = Stance.where(latest: true).where.not(cast_at: nil).pluck(:poll_id)
  41. chain = chain.where.not(id: voted_poll_ids)
  42. end
  43. 8 chain = chain.where(template: true) if params[:template]
  44. 8 chain = chain.where(author_id: params[:author_id]) if params[:author_id]
  45. 8 chain = chain.where(poll_type: params[:poll_type]) if params[:poll_type]
  46. 8 chain = chain.send(params[:status]) if %w(active closed recent template).include?(params[:status])
  47. 8 chain = chain.search_for(params[:query]) if params[:query]
  48. 8 chain
  49. end
  50. end

app/queries/reaction_query.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class ReactionQuery
  2. 1 def self.start
  3. 2 Reaction.includes(:user)
  4. end
  5. 1 def self.authorize!(user: LoggedOutUser.new, chain: start, params: )
  6. 2 discussion_ids = []
  7. 2 poll_ids = []
  8. 2 discussion_ids.concat(Comment.where(id: params[:comment_ids]).pluck(:discussion_id)) if params[:comment_ids]
  9. 2 discussion_ids.concat(params[:discussion_ids]) if params[:discussion_ids]
  10. 2 poll_ids.concat(Stance.where(id: params[:stance_ids]).pluck(:poll_id)) if params[:stance_ids]
  11. 2 poll_ids.concat(Outcome.where(id: params[:outcome_ids]).pluck(:poll_id)) if params[:outcome_ids]
  12. 2 poll_ids.concat(params[:poll_ids]) if params[:poll_ids]
  13. 2 discussion_ids.uniq!
  14. 2 poll_ids.uniq!
  15. 2 if (PollQuery.visible_to(user: user, show_public: true).where(id: poll_ids).count != poll_ids.length) ||
  16. 2 (DiscussionQuery.visible_to(user: user).where(id: discussion_ids).count != discussion_ids.length)
  17. 1 raise CanCan::AccessDenied.new
  18. end
  19. end
  20. 1 def self.unsafe_where(params)
  21. ids = {
  22. 1 discussion_ids: Array(params[:discussion_ids]),
  23. outcome_ids: Array(params[:outcome_ids]),
  24. comment_ids: Array(params[:comment_ids]),
  25. poll_ids: Array(params[:poll_ids]),
  26. stance_ids: Array(params[:stance_ids])
  27. }
  28. 1 Reaction.where(
  29. "(reactable_type = 'Discussion' AND reactable_id IN (:discussion_ids)) OR
  30. (reactable_type = 'Comment' AND reactable_id IN (:comment_ids)) OR
  31. (reactable_type = 'Outcome' AND reactable_id IN (:outcome_ids)) OR
  32. (reactable_type = 'Stance' AND reactable_id IN (:stance_ids)) OR
  33. (reactable_type = 'Poll' AND reactable_id IN (:poll_ids))", ids)
  34. end
  35. end

app/queries/user_query.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 1 class UserQuery
  2. 1 def self.relations(model:, actor:)
  3. 1731 rels = []
  4. 1731 if model.is_a?(Group) and model.members.exists?(actor.id)
  5. 13 rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
  6. where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: model.group.id})
  7. end
  8. 1731 if model.nil? or actor.can?(:add_guests, model)
  9. 1591 group_ids = if model && model.group.present? && (!model.is_a?(Group) || model.parent_id)
  10. 1559 actor.group_ids & model.group.parent_or_self.id_and_subgroup_ids
  11. else
  12. 32 actor.group_ids
  13. end
  14. 1591 rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
  15. where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: group_ids})
  16. # people who have been invited by actor
  17. 1591 rels.push(
  18. User.joins("LEFT OUTER JOIN discussion_readers dr on dr.user_id = users.id").
  19. where("dr.inviter_id = ? AND revoked_at IS NULL AND guest = TRUE", actor.id)
  20. )
  21. 1591 rels.push(
  22. User.joins("LEFT OUTER JOIN stances on stances.participant_id = users.id").
  23. where("stances.inviter_id = ? AND revoked_at IS NULL AND guest = TRUE", actor.id)
  24. )
  25. end
  26. 1731 if model.present? && (actor.can?(:add_members, model) || actor.can?(:add_voters, model))
  27. 1603 if model.group.present?
  28. 1581 rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
  29. where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: model.group.id})
  30. end
  31. 1603 if model.discussion_id
  32. 1200 rels.push(
  33. User.joins('LEFT OUTER JOIN discussion_readers dr ON dr.user_id = users.id').
  34. where('dr.discussion_id': model.discussion_id).where('dr.revoked_at IS NULL and dr.guest = TRUE')
  35. )
  36. 1200 rels.push(
  37. User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id').
  38. where('stances.poll_id': model.discussion.poll_ids).where("stances.revoked_at IS NULL and stances.guest = TRUE")
  39. )
  40. end
  41. 1603 if model.poll_id
  42. 795 rels.push(
  43. User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id').
  44. where('stances.poll_id': model.poll_id).where("stances.revoked_at IS NULL AND stances.guest = TRUE")
  45. )
  46. end
  47. end
  48. 1731 rels
  49. end
  50. 1 def self.invitable_user_ids(model: , actor:, user_ids: )
  51. 1704 relations(model: model, actor: actor).map do |rel|
  52. 9468 rel.where(id: user_ids).pluck(:id)
  53. end.flatten.uniq.compact
  54. end
  55. 1 def self.invitable_search(model:, actor:, q: nil, limit: 50)
  56. 27 ids = relations(model: model, actor: actor).map do |rel|
  57. 94 rel.active.search_for(q).limit(limit).pluck(:id)
  58. end.flatten.uniq.compact
  59. 27 User.where(id: ids).order(:memberships_count).limit(50)
  60. end
  61. end

app/serializers/application_serializer.rb

91.36% lines covered

81 relevant lines. 74 lines covered and 7 lines missed.
    
  1. 1 class ApplicationSerializer < ActiveModel::Serializer
  2. 1 embed :ids, include: true
  3. 1 def scope
  4. 206867 super || {}
  5. end
  6. 1 def tags
  7. 8091 cache_fetch([:tags_by_type_and_id, object.class.to_s], object.id) { object.tags }
  8. end
  9. 1 def poll
  10. 2334 cache_fetch(:polls_by_id, object.poll_id) { object.poll }
  11. end
  12. 1 def group
  13. 4398 cache_fetch(:groups_by_id, object.group_id) { object.group }
  14. end
  15. 1 def event
  16. 2852 cache_fetch(:events_by_id, object.event_id)
  17. end
  18. 1 def discussion
  19. 7974 cache_fetch(:discussions_by_id, object.discussion_id) { object.discussion }
  20. end
  21. 1 def author
  22. 5145 cache_fetch(:users_by_id, object.author_id) { object.author }
  23. end
  24. 1 def actor
  25. 10889 cache_fetch(:users_by_id, object.actor_id) { object.actor }
  26. end
  27. 1 def user
  28. 411 cache_fetch(:users_by_id, object.user_id) { object.user }
  29. end
  30. 1 def inviter
  31. 262 cache_fetch(:users_by_id, object.inviter_id) { object.inviter }
  32. end
  33. 1 def self.hide_when_discarded(names)
  34. 3 Array(names).each do |name|
  35. 36 define_method name do
  36. 13711 object.discarded_at ? nil : object.send(name)
  37. end
  38. end
  39. end
  40. 1 def cache_fetch(key_or_keys, id)
  41. 90818 return nil if id.nil?
  42. 80929 if scope.has_key?(:cache)
  43. 102640 scope[:cache].fetch(key_or_keys, id) { yield }
  44. else
  45. 222 yield
  46. end
  47. end
  48. 1 def include_type?(type)
  49. 23800 !Array(scope[:exclude_types]).include?(type)
  50. end
  51. 1 def exclude_type?(type)
  52. 1708 Array(scope[:exclude_types]).include?(type)
  53. end
  54. 1 def include_reactions?
  55. include_type?('reaction')
  56. end
  57. 1 def include_current_user_membership?
  58. 1019 include_type?('membership')
  59. end
  60. 1 def include_discussion?
  61. 3292 include_type?('discussion')
  62. end
  63. 1 def include_poll?
  64. 190 include_type?('poll')
  65. end
  66. 1 def include_created_event?
  67. 1362 include_type?('event')
  68. end
  69. 1 def include_forked_event?
  70. 990 include_type?('event')
  71. end
  72. 1 def include_group?
  73. 1480 include_type?('group') && object.group_id
  74. end
  75. 1 def include_active_polls?
  76. 990 include_type?('poll')
  77. end
  78. 1 def include_eventable?
  79. true
  80. end
  81. 1 def include_poll_options?
  82. 372 include_type?('poll_option')
  83. end
  84. 1 def include_stances?
  85. include_type?('stance')
  86. end
  87. 1 def include_my_stance?
  88. include_type?('stance') && scope[:current_user_id].present?
  89. end
  90. 1 def include_stance_choices?
  91. include_type?('stance_choice')
  92. end
  93. 1 def include_tags?
  94. 2411 include_type?('tag')
  95. end
  96. 1 def include_participant?
  97. 127 include_author?
  98. end
  99. 1 def include_user?
  100. 136 include_type?('user')
  101. end
  102. 1 def include_author?
  103. 1819 include_type?('user') and include_type?('author')
  104. end
  105. 1 def include_parent?
  106. 3697 include_type?('parent')
  107. end
  108. 1 def include_actor?
  109. 3616 include_type?('user')
  110. end
  111. 1 def include_inviter?
  112. 118 include_type?('user') && include_type?('inviter')
  113. end
  114. 1 def include_outcome?
  115. include_type?('outcome')
  116. end
  117. 1 def include_current_outcome?
  118. 372 include_type?('outcome')
  119. end
  120. 1 def include_outcomes?
  121. include_type?('outcome')
  122. end
  123. end

app/serializers/attachment_serializer.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class AttachmentSerializer < ActiveModel::Serializer
  2. 1 embed :ids, include: true
  3. 1 attributes :id, :filename, :content_type, :byte_size, :icon,
  4. :preview_url, :download_url, :created_at, :record_type, :record_id
  5. 1 has_one :record, polymorphic: true
  6. 1 has_one :author, serializer: AuthorSerializer, root: :users
  7. 1 def author
  8. 3 object.record.author
  9. end
  10. 1 def preview_url
  11. 1 Rails.application.routes.url_helpers.rails_representation_path(object.representation(HasRichText::PREVIEW_OPTIONS), only_path: true) if object.representable?
  12. end
  13. 1 def download_url
  14. 1 Rails.application.routes.url_helpers.rails_blob_path(object, only_path: true)
  15. end
  16. 1 def filename
  17. 1 object.blob.filename
  18. end
  19. 1 def content_type
  20. 3 object.blob.content_type
  21. end
  22. 1 def byte_size
  23. 1 object.blob.byte_size
  24. end
  25. 1 def icon
  26. 3 AppConfig.doctypes.detect{ |type| /#{type['regex']}/.match(content_type || filename) }['icon']
  27. end
  28. end

app/serializers/author_serializer.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 class AuthorSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :name,
  4. :email,
  5. :username,
  6. :avatar_initials,
  7. :avatar_kind,
  8. :thumb_url,
  9. :time_zone,
  10. :locale,
  11. :created_at,
  12. :titles,
  13. :placeholder_name,
  14. :email_verified,
  15. :bot
  16. 1 def include_email?
  17. 2509 scope[:current_user_id] == object.id || scope[:include_email] || scope[:current_user_is_admin]
  18. end
  19. 1 def titles
  20. 2522 object.experiences['titles'] || {}
  21. end
  22. 1 def avatar_kind
  23. 2522 if !object.email_verified && !object.name
  24. 8 'mdi-email-outline'
  25. else
  26. 2514 object.avatar_kind
  27. end
  28. end
  29. 1 def placeholder_name
  30. 8 I18n.t("user.placeholder_name", hostname: object.email.to_s.split('@').last, locale: object.locale)
  31. end
  32. 1 def include_placeholder_name?
  33. 2522 object.name.nil?
  34. end
  35. 1 private
  36. 1 def scope
  37. 7220 super || {}
  38. end
  39. end

app/serializers/chatbot_serializer.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class ChatbotSerializer < ApplicationSerializer
  2. attributes :id, :kind, :webhook_kind, :group_id, :server, :channel, :event_kinds, :name, :notification_only
  3. def include_server?
  4. scope && scope[:current_user_is_admin]
  5. end
  6. def include_channel?
  7. include_server?
  8. end
  9. end

app/serializers/comment_serializer.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 class CommentSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :body,
  4. :body_format,
  5. :mentioned_usernames,
  6. :discussion_id,
  7. :created_at,
  8. :updated_at,
  9. :parent_id,
  10. :parent_type,
  11. :content_locale,
  12. :versions_count,
  13. :attachments,
  14. :link_previews,
  15. :author_id,
  16. :discarded_at
  17. 1 has_one :author, serializer: AuthorSerializer, root: :users
  18. 1 has_one :discussion, serializer: DiscussionSerializer, root: :discussions
  19. 1 hide_when_discarded [:body]
  20. 1 def include_mentioned_usernames?
  21. 242 body_format == "md"
  22. end
  23. 1 def include_secret_token?
  24. object.user_id == scope[:current_user_id]
  25. end
  26. end

app/serializers/contact_message_serializer.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class ContactMessageSerializer < ActiveModel::Serializer
  2. attributes :name, :email, :message
  3. end

app/serializers/contact_request_serializer.rb

0.0% lines covered

2 relevant lines. 0 lines covered and 2 lines missed.
    
  1. class ContactRequestSerializer < ActiveModel::Serializer
  2. end

app/serializers/contact_serializer.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class ContactSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :id,
  4. :name,
  5. :email,
  6. :source
  7. has_one :user, serializer: UserSerializer, root: :users
  8. end

app/serializers/current_user_serializer.rb

81.82% lines covered

11 relevant lines. 9 lines covered and 2 lines missed.
    
  1. 1 class CurrentUserSerializer < UserSerializer
  2. 1 attributes :email, :email_when_proposal_closing_soon, :email_catch_up_day,
  3. :email_when_mentioned, :email_on_participation, :selected_locale,
  4. :locale, :default_membership_volume, :experiences,
  5. :email_newsletter, :is_admin, :memberships_count, :secret_token
  6. 1 def include_email?
  7. 13 true
  8. end
  9. 1 def include_email_hash?
  10. true
  11. end
  12. 1 def include_has_password?
  13. 13 true
  14. end
  15. 1 private
  16. 1 def from_scope(field)
  17. Array(Hash(scope)[field])
  18. end
  19. end

app/serializers/demo_serializer.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class DemoSerializer < ApplicationSerializer
  2. attributes :id,
  3. :name,
  4. :description,
  5. :group_id,
  6. :priority,
  7. :demo_handle
  8. has_one :author, serializer: AuthorSerializer, root: :users
  9. has_one :group
  10. end

app/serializers/discussion_reader_serializer.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class DiscussionReaderSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :user_id,
  4. :discussion_id,
  5. :read_ranges,
  6. :last_read_at,
  7. :dismissed_at,
  8. :volume,
  9. :inviter_id,
  10. :guest,
  11. :admin,
  12. :revoked_at
  13. 1 has_one :user, serializer: AuthorSerializer, root: :users
  14. # has_one :discussion, serializer: DiscussionSerializer, root: :discussions
  15. 1 def last_read_at
  16. 8 object.discussion.anonymous_polls_count == 0 ? object.last_read_at : nil
  17. end
  18. 1 def read_ranges
  19. 8 object.discussion.anonymous_polls_count == 0 ? object.read_ranges : []
  20. end
  21. 1 def volume
  22. 8 object[:volume]
  23. end
  24. end

app/serializers/discussion_serializer.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. 1 class DiscussionSerializer < ApplicationSerializer
  2. 1 def self.attributes_from_reader(*attrs)
  3. 1 attrs.each do |attr|
  4. 9 case attr
  5. 155 when :discussion_reader_id then define_method attr, -> { reader.id }
  6. 1240 else define_method attr, -> { reader.send(attr) }
  7. end
  8. 8919 define_method :"include_#{attr}?", -> { reader.present? }
  9. end
  10. 1 attributes *attrs
  11. end
  12. 1 attributes :id,
  13. :key,
  14. :group_id,
  15. :title,
  16. :tags,
  17. :content_locale,
  18. :description,
  19. :description_format,
  20. :discussion_template_id,
  21. :ranges,
  22. :items_count,
  23. :last_comment_at,
  24. :last_activity_at,
  25. :closed_at,
  26. :closer_id,
  27. :seen_by_count,
  28. :members_count,
  29. :created_at,
  30. :updated_at,
  31. :private,
  32. :versions_count,
  33. :pinned_at,
  34. :attachments,
  35. :link_previews,
  36. :mentioned_usernames,
  37. :newest_first,
  38. :max_depth,
  39. :discarded_at,
  40. :secret_token
  41. 1 attributes_from_reader :discussion_reader_id,
  42. :discussion_reader_volume,
  43. :discussion_reader_user_id,
  44. :last_read_at,
  45. :dismissed_at,
  46. :read_ranges,
  47. :revoked_at,
  48. :inviter_id,
  49. :admin
  50. 1 has_one :author, serializer: AuthorSerializer, root: :users
  51. 1 has_one :group, serializer: GroupSerializer, root: :groups
  52. 1 has_many :active_polls, serializer: PollSerializer, root: :polls
  53. 1 has_one :created_event, serializer: EventSerializer, root: :events
  54. 1 has_one :forked_event, serializer: EventSerializer, root: :events
  55. 1 has_one :closer, serializer: AuthorSerializer, root: :users
  56. 1 hide_when_discarded [:description, :title]
  57. 1 def include_closer?
  58. 990 object.closer_id.present?
  59. end
  60. 1 def include_mentioned_usernames?
  61. 990 description_format == "md"
  62. end
  63. 1 def active_polls
  64. 5121 cache_fetch(:polls_by_discussion_id, object.id) { [] }
  65. end
  66. 1 def reader
  67. # we don't initialize readers if no current user id, because discussions can be group messages
  68. 10296 cache_fetch(:discussion_readers_by_discussion_id, object.id) do
  69. 8856 return nil unless scope[:current_user_id]
  70. 1656 m = cache_fetch(:memberships_by_group_id, object.group_id) { nil }
  71. 1332 DiscussionReader.find_or_initialize_by(user_id: scope[:current_user_id], discussion_id: object.id) do |dr|
  72. 1314 dr.volume = (m && m.volume) || 'normal'
  73. end
  74. end
  75. end
  76. 1 def created_event
  77. 3136 cache_fetch([:events_by_kind_and_eventable_id, 'new_discussion'], object.id) { object.created_event }
  78. end
  79. 1 def forked_event
  80. 3960 cache_fetch([:events_by_kind_and_eventable_id, 'discussion_forked'], object.id) { nil }
  81. end
  82. end

app/serializers/discussion_template_serializer.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. class DiscussionTemplateSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. has_one :group, serializer: GroupSerializer, root: :groups
  4. has_many :poll_templates, serializer: PollTemplateSerializer, root: :poll_templates
  5. attributes :id,
  6. :group_id,
  7. :position,
  8. :author_id,
  9. :process_name,
  10. :process_subtitle,
  11. :process_introduction,
  12. :process_introduction_format,
  13. :tags,
  14. :title,
  15. :title_placeholder,
  16. :description,
  17. :description_format,
  18. :content_locale,
  19. :created_at,
  20. :updated_at,
  21. :discarded_at,
  22. :max_depth,
  23. :newest_first,
  24. :poll_template_keys_or_ids,
  25. :public,
  26. :recipient_audience
  27. end

app/serializers/document_serializer.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class DocumentSerializer < ApplicationSerializer
  2. 1 attributes :id, :title, :icon, :color, :url, :download_url,
  3. :web_url, :thumb_url, :model_id, :model_type,
  4. :created_at, :group_id
  5. 1 has_one :author, serializer: AuthorSerializer, root: :users
  6. 1 def group_id
  7. 18 object.group&.id
  8. end
  9. 1 def is_an_image?
  10. 36 object.doctype == 'image'
  11. end
  12. 1 alias :include_web_url? :is_an_image?
  13. 1 alias :include_thumb_url? :is_an_image?
  14. end

app/serializers/event_serializer.rb

91.43% lines covered

35 relevant lines. 32 lines covered and 3 lines missed.
    
  1. 1 class EventSerializer < ApplicationSerializer
  2. 1 attributes :id, :sequence_id, :position, :depth, :child_count, :descendant_count, :kind,
  3. :discussion_id, :created_at, :eventable_id, :eventable_type, :custom_fields,
  4. :pinned, :pinned_title, :parent_id, :actor_id, :position_key, :recipient_message
  5. 1 has_one :actor, serializer: AuthorSerializer, root: :users
  6. 1 has_one :eventable, polymorphic: true
  7. 1 has_one :discussion, serializer: DiscussionSerializer, root: :discussions
  8. 1 has_one :parent, serializer: EventSerializer, root: :parent_events
  9. # for discussion moved event
  10. 1 has_one :source_group, serializer: GroupSerializer, root: :groups
  11. 1 def parent
  12. 6195 cache_fetch(:events_by_id, object.parent_id) { object.parent }
  13. end
  14. 1 def include_eventable?
  15. 2678 !(object.kind == "new_discussion" && exclude_type?('discussion'))
  16. end
  17. 1 def eventable
  18. 16020 case object.eventable_type
  19. 10692 when 'Discussion' then cache_fetch(:discussions_by_id, object.eventable_id) { object.eventable }
  20. 3540 when 'Poll' then cache_fetch(:polls_by_id, object.eventable_id) { object.eventable }
  21. 1464 when 'Comment' then cache_fetch(:comments_by_id, object.eventable_id) { object.eventable }
  22. 438 when 'Stance' then cache_fetch(:stances_by_id, object.eventable_id) { object.eventable }
  23. 186 when 'Outcome' then cache_fetch(:outcomes_by_id, object.eventable_id) { object.eventable }
  24. 30 when 'Reaction' then cache_fetch(:reactions_by_id, object.eventable_id) { object.eventable }
  25. 66 when 'Membership' then cache_fetch(:memberships_by_id, object.eventable_id) { object.eventable }
  26. when 'Group' then cache_fetch(:groups_by_id, object.eventable_id) { object.eventable }
  27. when 'MembershipRequest' then cache_fetch(:membership_requests_by_id, object.eventable_id) { object.eventable }
  28. else
  29. # raise "waht is it? #{object.eventable} #{object.kind}"
  30. object.eventable
  31. end
  32. end
  33. 1 def position_key
  34. 2678 if object.kind == "new_discussion"
  35. 1708 "00000"
  36. else
  37. 970 object.position_key
  38. end
  39. end
  40. 1 def source_group
  41. 24 Group.find_by(id: object.custom_fields['source_group_id'])
  42. end
  43. 1 def include_source_group?
  44. 2678 object.kind == "discussion_moved" && object.custom_fields['source_group_id'].present?
  45. end
  46. 1 def pinned_title
  47. 2678 object.custom_fields['pinned_title']
  48. end
  49. 1 def include_custom_fields?
  50. 2678 ["poll_edited", "discussion_edited", "discussion_moved"].include? object.kind
  51. end
  52. end

app/serializers/group_serializer.rb

93.55% lines covered

31 relevant lines. 29 lines covered and 2 lines missed.
    
  1. 1 class GroupSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :key,
  4. :handle,
  5. :name,
  6. :full_name,
  7. :content_locale,
  8. :description,
  9. :description_format,
  10. :logo_url,
  11. :created_at,
  12. :creator_id,
  13. :members_can_add_members,
  14. :members_can_add_guests,
  15. :members_can_announce,
  16. :members_can_create_subgroups,
  17. :members_can_start_discussions,
  18. :members_can_edit_discussions,
  19. :members_can_edit_comments,
  20. :members_can_delete_comments,
  21. :members_can_raise_motions,
  22. :admins_can_edit_user_content,
  23. :token,
  24. :polls_count,
  25. :poll_templates_count,
  26. :closed_polls_count,
  27. :discussions_count,
  28. :public_discussions_count,
  29. :group_privacy,
  30. :memberships_count,
  31. :pending_memberships_count,
  32. :accepted_memberships_count,
  33. :membership_granted_upon,
  34. :discussion_privacy_options,
  35. :admin_memberships_count,
  36. :archived_at,
  37. :attachments,
  38. :link_previews,
  39. :new_threads_max_depth,
  40. :new_threads_newest_first,
  41. :cover_url,
  42. :open_discussions_count,
  43. :closed_discussions_count,
  44. :discussion_templates_count,
  45. :recent_activity_count,
  46. :is_visible_to_public,
  47. :is_visible_to_parent_members,
  48. :parent_members_can_see_discussions,
  49. :org_discussions_count,
  50. :org_members_count,
  51. :subscription,
  52. :subgroups_count,
  53. :new_host,
  54. :secret_token,
  55. :categorize_poll_templates
  56. 1 has_one :parent, serializer: GroupSerializer, root: :parent_groups
  57. 1 has_one :current_user_membership, serializer: MembershipSerializer, root: :memberships
  58. 1 has_many :tags, serializer: TagSerializer, root: :tags
  59. 1 def current_user_membership
  60. 6017 cache_fetch(:memberships_by_group_id, object.id) { nil }
  61. end
  62. 1 def parent
  63. 2069 cache_fetch(:groups_by_id, object.parent_id) { object.parent }
  64. end
  65. 1 def subscription
  66. 1216 sub = cache_fetch(:subscriptions_by_group_id, object.id) { object.subscription || Subscription.new }
  67. {
  68. 1019 max_members: sub.max_members,
  69. max_threads: sub.max_threads,
  70. allow_subgroups: sub.allow_subgroups,
  71. plan: sub.plan,
  72. state: sub.state,
  73. active: sub.is_active?,
  74. renews_at: sub.renews_at,
  75. expires_at: sub.expires_at,
  76. members_count: sub.members_count
  77. }
  78. end
  79. 1 def include_secret_token?
  80. 1019 current_user_membership && current_user_membership.admin
  81. end
  82. 1 def logo_url
  83. 1019 object.self_or_parent_logo_url
  84. end
  85. 1 def cover_url
  86. 1019 object.self_or_parent_cover_url
  87. end
  88. 1 def tag_names
  89. object.info['tag_names'] || []
  90. end
  91. 1 def new_host
  92. 1019 object.info['new_host'] || nil
  93. end
  94. 1 private
  95. 1 def include_org_members_count?
  96. 1019 object.is_parent?
  97. end
  98. 1 def include_org_discussions_count?
  99. 1019 object.is_parent?
  100. end
  101. 1 def include_token?
  102. 1019 Hash(scope)[:include_token]
  103. end
  104. 1 def has_discussions
  105. object.discussions_count > 0
  106. end
  107. end

app/serializers/identity_serializer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class IdentitySerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :id, :identity_type, :user_id, :name, :email, :logo, :custom_fields
  4. end

app/serializers/locale_serializer.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class LocaleSerializer < ActiveModel::Serializer
  2. attributes :key, :name
  3. def key
  4. object
  5. end
  6. def name
  7. I18n.with_locale(:en) { I18n.t(object, scope: :native_language_name) }
  8. end
  9. end

app/serializers/marked_as_read/discussion_serializer.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. class MarkedAsRead::DiscussionSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. def self.attributes_from_reader(*attrs)
  4. attrs.each do |attr|
  5. case attr
  6. when :discussion_reader_id then define_method attr, -> { reader.id }
  7. else define_method attr, -> { reader.send(attr) }
  8. end
  9. define_method :"include_#{attr}?", -> { reader.present? }
  10. end
  11. attributes *attrs
  12. end
  13. attributes :id,
  14. :key,
  15. :items_count,
  16. :ranges
  17. attributes_from_reader :discussion_reader_id,
  18. :read_ranges,
  19. :last_read_at,
  20. :dismissed_at
  21. def reader
  22. @reader ||= scope[:reader_cache].get_for(object) if scope[:reader_cache]
  23. end
  24. def scope
  25. super || {}
  26. end
  27. end

app/serializers/member_email_alias_serializer.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. 1 class MemberEmailAliasSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :email,
  4. :group_id,
  5. :user_id,
  6. :author_id,
  7. :created_at
  8. 1 has_one :user, serializer: AuthorSerializer, root: :users, key: :user_id
  9. 1 has_one :author, serializer: AuthorSerializer, root: :users
  10. 1 def user_email
  11. (cache_fetch(:users_by_id, object.user_id) { object.user }).email
  12. end
  13. 1 def include_user_email?
  14. true
  15. end
  16. end

app/serializers/member_serializer.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class MemberSerializer < ActiveModel::Serializer
  2. attributes :key, :priority, :type, :title, :subtitle, :logo_url, :logo_type, :last_notified_at
  3. end

app/serializers/membership_request_serializer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class MembershipRequestSerializer < ApplicationSerializer
  2. 1 attributes :id, :group_id, :name, :email, :introduction, :responded_at, :response, :created_at, :updated_at, :requestor_email
  3. 1 has_one :responder, serializer: AuthorSerializer, root: :users
  4. 1 has_one :requestor, serializer: AuthorSerializer, root: :users
  5. 1 def requestor_email
  6. 4 requestor&.email
  7. end
  8. end

app/serializers/membership_serializer.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class MembershipSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :group_id,
  4. :user_id,
  5. :inviter_id,
  6. :volume,
  7. :admin,
  8. :experiences,
  9. :title,
  10. :created_at,
  11. :accepted_at,
  12. :user_email
  13. 1 has_one :group, serializer: GroupSerializer, root: :groups
  14. 1 has_one :user, serializer: UserSerializer, root: :users, key: :user_id
  15. 1 has_one :inviter, serializer: AuthorSerializer, root: :users
  16. 1 def user_email
  17. 20 (cache_fetch(:users_by_id, object.user_id) { object.user }).email
  18. end
  19. 1 def include_user_email?
  20. 118 scope && (
  21. 118 object.inviter_id == scope[:current_user_id] ||
  22. scope[:current_user_is_admin]
  23. )
  24. end
  25. end

app/serializers/metadata/discussion_serializer.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class Metadata::DiscussionSerializer < MetadataSerializer
  2. attributes :title, :description, :image_urls
  3. def description
  4. render_plain_text(object.description, object.description_format)
  5. end
  6. def image_urls
  7. [object.group.cover_url, object.group.logo_url]
  8. end
  9. end

app/serializers/metadata/group_serializer.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class Metadata::GroupSerializer < MetadataSerializer
  2. attributes :title, :description, :image_urls
  3. def title
  4. object.full_name
  5. end
  6. def description
  7. if object.is_visible_to_public?
  8. render_plain_text(object.description, object.description_format)
  9. end
  10. end
  11. def image_urls
  12. [object.group.cover_url, object.group.logo_url]
  13. end
  14. end

app/serializers/metadata/poll_serializer.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class Metadata::PollSerializer < MetadataSerializer
  2. attributes :title, :description, :image_urls
  3. def title
  4. object.title
  5. end
  6. def description
  7. render_plain_text(object.details, object.details_format)
  8. end
  9. def image_urls
  10. object.group ? [object.group.cover_url, object.group.logo_url] : []
  11. end
  12. end

app/serializers/metadata/user_serializer.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class Metadata::UserSerializer < MetadataSerializer
  2. attributes :title, :description, :image_urls
  3. def title
  4. object.name
  5. end
  6. def description
  7. render_plain_text(object.short_bio, object.short_bio_format)
  8. end
  9. def image_urls
  10. [object.avatar_url]
  11. end
  12. end

app/serializers/metadata_serializer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class MetadataSerializer < ActiveModel::Serializer
  2. include EmailHelper
  3. root false
  4. end

app/serializers/model_error_serializer.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class ModelErrorSerializer < ActiveModel::Serializer
  2. attributes :id, :messages
  3. def messages
  4. object.errors.full_messages
  5. end
  6. end

app/serializers/notification/event_serializer.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Notification::EventSerializer < EventSerializer
  2. def include_discussion?
  3. false
  4. end
  5. def include_parent?
  6. false
  7. end
  8. end

app/serializers/notification_serializer.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. 1 class NotificationSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :viewed,
  4. :created_at,
  5. :url,
  6. :kind,
  7. :actor_id,
  8. :event_id,
  9. :name,
  10. :title,
  11. :poll_type,
  12. :reaction,
  13. :model
  14. 1 has_one :actor, serializer: AuthorSerializer, root: :users
  15. 1 def name
  16. 938 tv :name
  17. end
  18. 1 def title
  19. 938 tv :title
  20. end
  21. 1 def poll_type
  22. 938 tv :poll_type
  23. end
  24. 1 def reaction
  25. 938 tv :reaction
  26. end
  27. 1 def model
  28. 938 tv :model
  29. end
  30. 1 def tv(key)
  31. 4690 object.translation_values[key.to_s]
  32. end
  33. 1 def kind
  34. 938 if event.kind == "announcement_created"
  35. event.custom_fields['kind'] || "group_announced"
  36. 938 elsif event.kind == 'user_mentioned' &&
  37. event.eventable.respond_to?(:parent) &&
  38. event.eventable.parent.present? &&
  39. event.eventable.parent.author == object.user
  40. 3 "comment_replied_to"
  41. else
  42. 935 event.kind
  43. end
  44. end
  45. end

app/serializers/outcome_serializer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class OutcomeSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :statement,
  4. :statement_format,
  5. :content_locale,
  6. :latest,
  7. :created_at,
  8. :event_summary,
  9. :event_location,
  10. :attachments,
  11. :link_previews,
  12. :event_summary,
  13. :review_on,
  14. :event_location,
  15. :poll_id,
  16. :poll_option_id,
  17. :group_id,
  18. :author_id,
  19. :secret_token,
  20. :versions_count
  21. 1 has_one :author, serializer: AuthorSerializer, root: :users
  22. 1 has_one :poll, serializer: PollSerializer, root: :polls
  23. 1 def group_id
  24. 33 (cache_fetch(:polls_by_id, poll_id) { object.poll }).group_id
  25. end
  26. end

app/serializers/pending/base_serializer.rb

0.0% lines covered

44 relevant lines. 0 lines covered and 44 lines missed.
    
  1. class Pending::BaseSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :name, :email, :email_status, :email_verified, :has_password, :identity_type,
  4. :avatar_kind, :avatar_initials, :thumb_url, :avatar_url, :has_token, :auth_form
  5. def identity_type
  6. false
  7. end
  8. def auth_form
  9. if user.email_status == :inactive && !has_token
  10. :inactive
  11. elsif (user.email_verified || has_token) && user.name
  12. :signIn
  13. else
  14. :signUp
  15. end
  16. end
  17. def has_token
  18. # pending login or invitation token
  19. end
  20. def avatar_url
  21. user.avatar_url
  22. end
  23. def thumb_url
  24. user.thumb_url
  25. end
  26. def avatar_kind
  27. user.avatar_kind
  28. end
  29. def avatar_initials
  30. user.avatar_initials
  31. end
  32. def email_status
  33. user.email_status
  34. end
  35. def email_verified
  36. user.email_verified
  37. end
  38. def has_password
  39. user.has_password
  40. end
  41. private
  42. def user
  43. @user ||= User.verified.find_by(email: email) || LoggedOutUser.new
  44. end
  45. end

app/serializers/pending/discussion_reader_serializer.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Pending::DiscussionReaderSerializer < Pending::MembershipSerializer
  2. def identity_type
  3. :discussion_reader
  4. end
  5. def group_id
  6. nil
  7. end
  8. end

app/serializers/pending/group_serializer.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. class Pending::GroupSerializer < Pending::BaseSerializer
  2. attributes :token, :group_id
  3. def auth_form
  4. false
  5. end
  6. def has_token
  7. true
  8. end
  9. def token
  10. object.token
  11. end
  12. def include_email_status?
  13. false
  14. end
  15. def include_email?
  16. false
  17. end
  18. def email
  19. nil
  20. end
  21. def name
  22. nil
  23. end
  24. def identity_type
  25. :group
  26. end
  27. def group_id
  28. object.id
  29. end
  30. end

app/serializers/pending/identity_serializer.rb

0.0% lines covered

18 relevant lines. 0 lines covered and 18 lines missed.
    
  1. class Pending::IdentitySerializer < Pending::BaseSerializer
  2. def auth_form
  3. :identity
  4. end
  5. def identity_type
  6. object.identity_type
  7. end
  8. def avatar_kind
  9. if object.logo.present?
  10. 'uploaded'
  11. else
  12. 'initials'
  13. end
  14. end
  15. def avatar_url
  16. object.logo
  17. end
  18. end

app/serializers/pending/membership_serializer.rb

0.0% lines covered

32 relevant lines. 0 lines covered and 32 lines missed.
    
  1. class Pending::MembershipSerializer < Pending::BaseSerializer
  2. attributes :token, :group_id
  3. def auth_form
  4. false
  5. end
  6. def identity_type
  7. :membership
  8. end
  9. def has_token
  10. true
  11. end
  12. def email_status
  13. nil
  14. end
  15. def avatar_initials
  16. object.user&.get_avatar_initials
  17. end
  18. def name
  19. object.user&.name
  20. end
  21. def email
  22. object.user&.email
  23. end
  24. def group_id
  25. object.group_id
  26. end
  27. private
  28. def has_name?
  29. object.user&.name.present?
  30. end
  31. alias :include_avatar_initials? :has_name?
  32. end

app/serializers/pending/stance_serializer.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class Pending::StanceSerializer < Pending::MembershipSerializer
  2. def identity_type
  3. :stance
  4. end
  5. def group_id
  6. nil
  7. end
  8. end

app/serializers/pending/token_serializer.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. class Pending::TokenSerializer < Pending::BaseSerializer
  2. attributes :legal_accepted_at
  3. def has_token
  4. true
  5. end
  6. def identity_type
  7. 'loomio'
  8. end
  9. def name
  10. if object.is_reactivation
  11. user[:name]
  12. else
  13. user.name
  14. end
  15. end
  16. def email
  17. user.email
  18. end
  19. def legal_accepted_at
  20. user.legal_accepted_at
  21. end
  22. private
  23. def user
  24. @user ||= object.user
  25. end
  26. def email_status
  27. return :active if object.is_reactivation
  28. User.email_status_for(email)
  29. end
  30. end

app/serializers/pending/user_serializer.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. class Pending::UserSerializer < Pending::BaseSerializer
  2. attributes :legal_accepted_at
  3. private
  4. def has_token
  5. Hash(scope)[:has_token]
  6. end
  7. def user
  8. object
  9. end
  10. def email_status
  11. User.email_status_for(object.email)
  12. end
  13. end

app/serializers/permitted_params_serializer.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class PermittedParamsSerializer < ActiveModel::Serializer
  2. root false
  3. def object
  4. PermittedParams.new
  5. end
  6. PermittedParams::MODELS.each do |kind|
  7. send :attribute, :"#{kind}_attributes", key: kind
  8. end
  9. end

app/serializers/plugin_serializer.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class PluginSerializer < ActiveModel::Serializer
  2. attributes :name, :config
  3. end

app/serializers/poll_option_serializer.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class PollOptionSerializer < ApplicationSerializer
  2. 1 attributes :id, :poll_id, :name, :priority, :color, :icon, :meaning, :prompt
  3. end

app/serializers/poll_serializer.rb

96.88% lines covered

32 relevant lines. 31 lines covered and 1 lines missed.
    
  1. 1 class PollSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :limit_reason_length,
  4. :attachments,
  5. :agree_target,
  6. :author_id,
  7. :anonymous,
  8. :can_respond_maybe,
  9. :chart_type,
  10. :chart_column,
  11. :closed_at,
  12. :closing_at,
  13. :created_at,
  14. :content_locale,
  15. :cast_stances_pct,
  16. :decided_voters_count,
  17. :details,
  18. :details_format,
  19. :discarded_at,
  20. :discarded_by,
  21. :discussion_id,
  22. :group_id,
  23. :hide_results,
  24. :key,
  25. :link_previews,
  26. :mentioned_usernames,
  27. :notify_on_closing_soon,
  28. :poll_type,
  29. :poll_option_names,
  30. :poll_option_name_format,
  31. :results,
  32. :result_columns,
  33. :reason_prompt,
  34. :shuffle_options,
  35. :stance_counts,
  36. :specified_voters_only,
  37. :secret_token,
  38. :total_score,
  39. :title,
  40. :tags,
  41. :undecided_voters_count,
  42. :voter_can_add_options,
  43. :voters_count,
  44. :stance_reason_required,
  45. :versions_count,
  46. :dots_per_person,
  47. :max_score,
  48. :min_score,
  49. :minimum_stance_choices,
  50. :maximum_stance_choices,
  51. :meeting_duration,
  52. :poll_template_id,
  53. :poll_template_key
  54. 1 has_one :discussion, serializer: DiscussionSerializer, root: :discussions
  55. 1 has_one :created_event, serializer: EventSerializer, root: :events
  56. 1 has_one :group, serializer: GroupSerializer, root: :groups
  57. 1 has_one :author, serializer: AuthorSerializer, root: :users
  58. 1 has_one :current_outcome, serializer: OutcomeSerializer, root: :outcomes
  59. 1 has_one :my_stance, serializer: StanceSerializer, root: :stances
  60. 1 has_many :poll_options, serializer: PollOptionSerializer, root: :poll_options
  61. 1 hide_when_discarded [
  62. :attachments,
  63. :link_previews,
  64. :author_id,
  65. :anonymous,
  66. :can_respond_maybe,
  67. :closed_at,
  68. :closing_at,
  69. :created_at,
  70. :content_locale,
  71. :cast_stances_pct,
  72. :decided_voters_count,
  73. :details,
  74. :details_format,
  75. :hide_results,
  76. :limit_reason_length,
  77. :notify_on_closing_soon,
  78. :poll_type,
  79. :poll_option_names,
  80. :mentioned_usernames,
  81. :results,
  82. :shuffle_options,
  83. :stance_counts,
  84. :total_score,
  85. :specified_voters_only,
  86. :secret_token,
  87. :title,
  88. :undecided_voters_count,
  89. :voter_can_add_options,
  90. :voters_count,
  91. :stance_reason_required,
  92. :versions_count,
  93. :meeting_duration,
  94. :default_duration_in_days
  95. ]
  96. 1 def include_stance_counts?
  97. 372 poll.show_results?(voted: true)
  98. end
  99. 1 def results
  100. 331 PollService.calculate_results(object, poll_options)
  101. end
  102. 1 def include_results?
  103. 372 poll.show_results?(voted: true)
  104. end
  105. 1 def current_outcome
  106. 1420 cache_fetch(:outcomes_by_poll_id, object.id) { nil }
  107. end
  108. 1 def poll_options
  109. 1475 cache_fetch(:poll_options_by_poll_id, object.id) { object.poll_options }
  110. end
  111. 1 def poll_option_names
  112. 377 cache_fetch(:poll_options_by_poll_id, object.id) { poll_options }.map(&:name)
  113. end
  114. 1 def created_event
  115. 1094 cache_fetch([:events_by_kind_and_eventable_id, 'poll_created'], object.id) { object.created_event }
  116. end
  117. 1 def include_mentioned_usernames?
  118. 372 details_format == "md"
  119. end
  120. 1 def removed_poll_option_ids
  121. object.poll_option_attributes.select { |attr| attr[:_destroy] }.map { |attr| attr[:id] }
  122. end
  123. 1 def my_stance
  124. 840 cache_fetch(:my_stances_by_poll_id, object.id) { Stance.latest.find_by(poll_id: object.id, participant_id: scope[:current_user_id]) }
  125. end
  126. 1 def include_my_stance?
  127. 372 my_stance.present?
  128. end
  129. end

app/serializers/poll_template_serializer.rb

0.0% lines covered

44 relevant lines. 0 lines covered and 44 lines missed.
    
  1. class PollTemplateSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. has_one :group, serializer: GroupSerializer, root: :groups
  4. attributes :id,
  5. :key,
  6. :group_id,
  7. :position,
  8. :author_id,
  9. :poll_type,
  10. :process_name,
  11. :process_subtitle,
  12. :process_introduction,
  13. :process_introduction_format,
  14. :tags,
  15. :title,
  16. :title_placeholder,
  17. :details,
  18. :details_format,
  19. :anonymous,
  20. :specified_voters_only,
  21. :notify_on_closing_soon,
  22. :content_locale,
  23. :shuffle_options,
  24. :hide_results,
  25. :chart_type,
  26. :min_score,
  27. :max_score,
  28. :minimum_stance_choices,
  29. :maximum_stance_choices,
  30. :dots_per_person,
  31. :reason_prompt,
  32. :poll_options,
  33. :poll_option_name_format,
  34. :stance_reason_required,
  35. :limit_reason_length,
  36. :default_duration_in_days,
  37. :agree_target,
  38. :created_at,
  39. :updated_at,
  40. :discarded_at,
  41. :outcome_statement,
  42. :outcome_statement_format,
  43. :outcome_review_due_in_days
  44. end

app/serializers/reaction_serializer.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ReactionSerializer < ApplicationSerializer
  2. 1 attributes :id, :reaction, :reactable_id, :reactable_type, :user_id
  3. 1 has_one :user, serializer: AuthorSerializer, root: :users
  4. end

app/serializers/received_email_serializer.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class ReceivedEmailSerializer < ApplicationSerializer
  2. 1 attributes :id, :group_id, :released, :subject, :sender_email, :sender_name, :dkim_valid, :spf_valid
  3. end

app/serializers/restricted/group_serializer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class Restricted::GroupSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :id, :name, :logo_url, :cover_url
  4. end

app/serializers/restricted/membership_serializer.rb

0.0% lines covered

5 relevant lines. 0 lines covered and 5 lines missed.
    
  1. class Restricted::MembershipSerializer < ApplicationSerializer
  2. embed :ids, include: true
  3. attributes :id, :volume, :user_id, :group_id
  4. has_one :group, serializer: Restricted::GroupSerializer, root: :groups
  5. end

app/serializers/restricted/user_serializer.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class Restricted::UserSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :id, :restricted, :username, :email, :email_when_proposal_closing_soon, :email_catch_up_day, :email_newsletter,
  4. :email_when_mentioned, :email_on_participation, :default_membership_volume, :unsubscribe_token, :locale, :deactivated_at
  5. has_many :memberships, serializer: Restricted::MembershipSerializer, root: :memberships
  6. def restricted
  7. true
  8. end
  9. end

app/serializers/search_result_serializer.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class SearchResultSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :searchable_type,
  4. :searchable_id,
  5. :poll_title,
  6. :discussion_title,
  7. :discussion_key,
  8. :highlight,
  9. :poll_key,
  10. :poll_id,
  11. :sequence_id,
  12. :group_id,
  13. :group_handle,
  14. :group_key,
  15. :group_name,
  16. :author_name,
  17. :author_id,
  18. :authored_at,
  19. :tags
  20. 1 has_one :author, serializer: AuthorSerializer, root: :users
  21. 1 has_one :poll, serializer: PollSerializer, root: :polls
  22. end

app/serializers/search_results/base_serializer.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class SearchResults::BaseSerializer < ActiveModel::Serializer
  2. attributes :id, :blurb
  3. end

app/serializers/search_results/comment_serializer.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class SearchResults::CommentSerializer < SearchResults::BaseSerializer
  2. has_one :author, serializer: UserSerializer
  3. def author
  4. User.find_by(id: object.user_id)
  5. end
  6. end

app/serializers/search_results/discussion_serializer.rb

0.0% lines covered

3 relevant lines. 0 lines covered and 3 lines missed.
    
  1. class SearchResults::DiscussionSerializer < ApplicationSerializer
  2. attributes :title, :created_at, :group_name, :group_full_name, :key, :last_activity_at
  3. end

app/serializers/stance_choice_serializer.rb

0.0% lines covered

19 relevant lines. 0 lines covered and 19 lines missed.
    
  1. class StanceChoiceSerializer < ApplicationSerializer
  2. attributes :id, :score, :created_at, :stance_id, :rank, :rank_or_score, :poll_option_id
  3. has_one :poll_option
  4. def poll_option
  5. cache_fetch(:poll_options_by_id, object.poll_option_id) { object.poll_option }
  6. end
  7. def stance
  8. cache_fetch(:stances_by_id, object.stance_id) { object.stance }
  9. end
  10. def poll
  11. cache_fetch(:polls_by_id, cache_fetch(:stances_by_id, object.stance_id).poll_id) { object.poll }
  12. end
  13. def rank
  14. poll.minimum_stance_choices - object.score + 1 if poll.poll_type == 'ranked_choice'
  15. end
  16. def rank_or_score
  17. rank || object.score
  18. end
  19. end

app/serializers/stance_serializer.rb

96.67% lines covered

30 relevant lines. 29 lines covered and 1 lines missed.
    
  1. 1 class StanceSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :reason,
  4. :reason_format,
  5. :content_locale,
  6. :latest,
  7. :admin,
  8. :cast_at,
  9. :mentioned_usernames,
  10. :created_at,
  11. :updated_at,
  12. :locale,
  13. :versions_count,
  14. :attachments,
  15. :link_previews,
  16. :volume,
  17. :inviter_id,
  18. :poll_id,
  19. :participant_id,
  20. :revoked_at,
  21. :order_at,
  22. :option_scores
  23. 1 has_one :poll, serializer: PollSerializer, root: :polls
  24. 1 has_one :participant, serializer: AuthorSerializer, root: :users
  25. 1 def order_at
  26. 127 object.cast_at || object.created_at
  27. end
  28. 1 def option_scores
  29. 111 if ENV['JIT_POLL_COUNTS'] && object.option_scores == {} && object.cast_at
  30. object.update_option_scores!
  31. end
  32. 111 object.option_scores
  33. end
  34. 1 def include_option_scores?
  35. 127 include_reason?
  36. end
  37. 1 def locale
  38. 127 participant&.locale || group&.locale
  39. end
  40. 1 def participant
  41. 486 return nil if poll.anonymous?
  42. 420 cache_fetch(:users_by_id, object.participant_id) { object.participant }
  43. end
  44. 1 def participant_id
  45. 127 return nil if poll.anonymous?
  46. 105 object.participant_id
  47. end
  48. 1 def volume
  49. 127 object[:volume]
  50. end
  51. 1 def include_reason?
  52. 635 !object.revoked_at && (object.participant_id == scope[:current_user_id] || poll.show_results?(voted: true))
  53. end
  54. 1 def include_mentioned_usernames?
  55. 127 include_reason? && reason_format == 'md'
  56. end
  57. 1 def include_attachments?
  58. 127 include_reason?
  59. end
  60. 1 def include_link_previews?
  61. 127 include_reason?
  62. end
  63. end

app/serializers/tag_serializer.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class TagSerializer < ApplicationSerializer
  2. 1 attributes :id, :name, :color, :taggings_count, :org_taggings_count, :group_id, :priority
  3. end

app/serializers/task_serializer.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class TaskSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :name,
  4. :author_id,
  5. :uid,
  6. :done,
  7. :done_at,
  8. :due_on,
  9. :record_type,
  10. :record_id
  11. 1 has_one :record, polymorphic: true, key: 'record_obj'
  12. 1 has_one :author, serializer: AuthorSerializer, root: :users
  13. end

app/serializers/translation_serializer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class TranslationSerializer < ActiveModel::Serializer
  2. embed :ids, include: true
  3. attributes :translatable_id, :translatable_type, :fields, :language
  4. end

app/serializers/user_serializer.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class UserSerializer < AuthorSerializer
  2. 1 attributes :short_bio,
  3. :short_bio_format,
  4. :content_locale,
  5. :location,
  6. :has_password,
  7. :autodetect_time_zone,
  8. :avatar_url,
  9. :attachments,
  10. :date_time_pref
  11. 1 def include_has_password?
  12. 89 scope[:include_password_status]
  13. end
  14. end

app/serializers/version_serializer.rb

63.64% lines covered

22 relevant lines. 14 lines covered and 8 lines missed.
    
  1. 1 class VersionSerializer < ApplicationSerializer
  2. 1 attributes :id,
  3. :whodunnit,
  4. :previous_id,
  5. :created_at,
  6. :item_id,
  7. :item_type,
  8. :object_changes
  9. # has_one :discussion
  10. # has_one :comment
  11. # has_one :poll
  12. # has_one :stance
  13. 1 def whodunnit
  14. 1 object.whodunnit.to_i
  15. end
  16. 1 def discussion
  17. object.item
  18. end
  19. 1 def poll
  20. object.item
  21. end
  22. 1 def comment
  23. object.item
  24. end
  25. 1 def stance
  26. object.item
  27. end
  28. 1 def previous_id
  29. 1 object.previous.try :id
  30. end
  31. 1 def include_discussion?
  32. object.item_type == 'Discussion'
  33. end
  34. 1 def include_poll?
  35. object.item_type == 'Poll'
  36. end
  37. 1 def include_comment?
  38. object.item_type == 'Comment'
  39. end
  40. 1 def include_stance?
  41. object.item_type == 'Stance'
  42. end
  43. end

app/serializers/webhook/discord/event_serializer.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class Webhook::Discord::EventSerializer < Webhook::Markdown::EventSerializer
  2. attributes :content
  3. def content
  4. text
  5. end
  6. end

app/serializers/webhook/markdown/event_serializer.rb

82.35% lines covered

17 relevant lines. 14 lines covered and 3 lines missed.
    
  1. 1 class Webhook::Markdown::EventSerializer < ActiveModel::Serializer
  2. 1 include PrettyUrlHelper
  3. 1 attributes :text,
  4. :icon_url,
  5. :username
  6. 1 def icon_url
  7. 38 (root_url(host: ENV['CANONICAL_HOST']).chomp('/') + (object.group.self_or_parent_logo_url(128) || ''))
  8. end
  9. 1 def attachments
  10. object.eventable.attachments
  11. end
  12. 1 def username
  13. 38 AppConfig.theme[:site_name]
  14. end
  15. 1 def text
  16. 38 I18n.with_locale(object.eventable.group.locale) do
  17. 38 ApplicationController.renderer.render(
  18. layout: nil,
  19. template: "chatbot/markdown/#{scope[:template_name]}",
  20. assigns: { poll: object.eventable.poll, event: object, recipient: scope[:recipient] } )
  21. end
  22. end
  23. 1 private
  24. 1 def user
  25. object.user || object.eventable.author
  26. end
  27. 1 def eventable
  28. object.eventable
  29. end
  30. end

app/serializers/webhook/microsoft/event_serializer.rb

0.0% lines covered

33 relevant lines. 0 lines covered and 33 lines missed.
    
  1. class Webhook::Microsoft::EventSerializer < Webhook::Markdown::EventSerializer
  2. attribute :type, key: :"@type"
  3. attribute :context, key: :"@context"
  4. attribute :theme_color, key: :themeColor
  5. attributes :text, :sections
  6. def type
  7. "MessageCard"
  8. end
  9. def context
  10. "http://schema.org/extensions"
  11. end
  12. def theme_color
  13. AppConfig.theme[:primary_color]
  14. end
  15. def sections
  16. []
  17. # [{
  18. # activityTitle: "[#{section_title}](#{section_url})",
  19. # activitySubtitle: section_subtitle,
  20. # activityImage: section_image,
  21. # facts: section_facts,
  22. # markdown: true
  23. # }]
  24. end
  25. def section_title
  26. text_options[:title]
  27. end
  28. def section_url
  29. text_options[:url]
  30. end
  31. def section_subtitle
  32. object.eventable.description
  33. end
  34. def section_image
  35. user.avatar_url
  36. end
  37. def section_facts
  38. []
  39. end
  40. end

app/serializers/webhook/slack/event_serializer.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. class Webhook::Slack::EventSerializer < Webhook::Markdown::EventSerializer
  2. def include_icon_url?
  3. false
  4. end
  5. def include_username?
  6. false
  7. end
  8. def text
  9. I18n.with_locale(object.eventable.group.locale) do
  10. ApplicationController.renderer.render(
  11. layout: nil,
  12. template: "chatbot/slack/#{scope[:template_name]}",
  13. assigns: { poll: object.eventable.poll, event: object, recipient: scope[:recipient] } )
  14. end
  15. end
  16. end

app/services/announcement_service.rb

45.0% lines covered

20 relevant lines. 9 lines covered and 11 lines missed.
    
  1. 1 class AnnouncementService
  2. 1 class UnknownAudienceKindError < Exception; end
  3. 1 def self.audience_users(model, kind, actor, exclude_members = false, include_actor = false)
  4. 182 users = case kind
  5. when /group-\d+/
  6. id = kind.match(/group-(\d+)/)[1].to_i
  7. group = model.group.parent_or_self.self_and_subgroups.find(id)
  8. raise CanCan::AccessDenied unless actor.can?(:notify, group)
  9. group.members
  10. 181 when 'group' then model.group.members
  11. when 'discussion_group' then (model.discussion || NullDiscussion.new).readers
  12. 1 when 'voters' then (model.poll || NullPoll.new).unmasked_voters
  13. when 'decided_voters' then (model.poll || NullPoll.new).unmasked_decided_voters
  14. when 'undecided_voters' then (model.poll || NullPoll.new).unmasked_undecided_voters
  15. when 'non_voters' then (model.poll || NullPoll.new).non_voters
  16. when nil then User.none
  17. else
  18. raise UnknownAudienceKindError.new
  19. end.active
  20. 182 users = users.where.not(id: (model.poll || NullPoll.new).voter_ids) if exclude_members
  21. 182 include_actor ? users.active.humans : users.active.humans.where('users.id != ?', actor.id)
  22. end
  23. 1 def self.resend_pending_invitations(since: 25.hours.ago, till: 24.hours.ago)
  24. Event.invitations_in_period(since, till).each { |event| Events::AnnouncementResend.publish!(event) }
  25. end
  26. end

app/services/chatbot_service.rb

57.78% lines covered

45 relevant lines. 26 lines covered and 19 lines missed.
    
  1. 1 class ChatbotService
  2. 1 def self.create(chatbot:, actor:)
  3. actor.ability.authorize! :create, chatbot
  4. return false unless chatbot.valid?
  5. chatbot.author = actor
  6. chatbot.save!
  7. end
  8. 1 def self.update(chatbot:, params:, actor:)
  9. actor.ability.authorize! :update, chatbot
  10. params.delete(:access_token) unless params[:access_token].present?
  11. chatbot.assign_attributes(params)
  12. return false unless chatbot.valid?
  13. chatbot.save!
  14. end
  15. 1 def self.destroy(chatbot:, actor:)
  16. actor.ability.authorize! :destroy, chatbot
  17. chatbot.destroy
  18. end
  19. 1 def self.publish_event!(event_id)
  20. 901 event = Event.find(event_id)
  21. 901 event.reload
  22. 901 return if event.eventable.nil?
  23. 901 chatbots = event.eventable.group.chatbots
  24. 901 CACHE_REDIS_POOL.with do |client|
  25. 901 chatbots.where(id: event.recipient_chatbot_ids).
  26. or(chatbots.where.any(event_kinds: event.kind)).each do |chatbot|
  27. # later, make a list and rpush into it. i guess
  28. 38 template_name = event.eventable_type.tableize.singularize
  29. 38 template_name = 'poll' if event.eventable_type == 'Outcome'
  30. 38 template_name = 'group' if event.eventable_type == 'Membership'
  31. 38 template_name = 'notification' if chatbot.notification_only
  32. 38 if %w[Poll Stance Outcome].include? event.eventable_type
  33. 16 poll = event.eventable.poll
  34. end
  35. 38 example_user = chatbot.author || chatbot.group.creator
  36. 38 recipient = LoggedOutUser.new(locale: example_user.locale,
  37. time_zone: example_user.time_zone,
  38. date_time_pref: example_user.date_time_pref)
  39. 38 I18n.with_locale(recipient.locale) do
  40. 38 if chatbot.kind == "webhook"
  41. 38 serializer = "Webhook::#{chatbot.webhook_kind.classify}::EventSerializer".constantize
  42. 38 payload = serializer.new(event, root: false, scope: {template_name: template_name, recipient: recipient}).as_json
  43. 38 req = Clients::Webhook.new.post(chatbot.server, params: payload)
  44. 38 if req.response.code != 200
  45. Sentry.capture_message("chatbot id #{chatbot.id} post event id #{event.id} failed: code: #{req.response.code} body: #{req.response.body}")
  46. end
  47. else
  48. client.publish("chatbot/publish", {
  49. config: chatbot.config,
  50. payload: {
  51. html: ApplicationController.renderer.render(
  52. layout: nil,
  53. template: "chatbot/matrix/#{template_name}",
  54. assigns: { poll: poll, event: event, recipient: recipient } )
  55. }
  56. }.to_json)
  57. end
  58. end
  59. end
  60. end
  61. end
  62. 1 def self.publish_test!(params)
  63. case params[:kind]
  64. when 'slack_webhook'
  65. Clients::Webhook.new.post(params[:server], params: {text: I18n.t('chatbot.connection_test_successful')})
  66. else
  67. MAIN_REDIS_POOL.with do |client|
  68. data = params.slice(:server, :access_token, :channel)
  69. data.merge!(message: I18n.t('chatbot.connection_test_successful', group: params[:group_name]))
  70. client.publish("chatbot/test", data.to_json)
  71. end
  72. end
  73. end
  74. end

app/services/cleanup_service.rb

0.0% lines covered

23 relevant lines. 0 lines covered and 23 lines missed.
    
  1. module CleanupService
  2. def self.delete_orphan_records
  3. [Group,
  4. Membership,
  5. MembershipRequest,
  6. Discussion,
  7. Subscription,
  8. DiscussionReader,
  9. Comment,
  10. Poll,
  11. PollOption,
  12. Stance,
  13. StanceChoice,
  14. Outcome,
  15. Event,
  16. Notification].each do |model|
  17. count = model.dangling.delete_all
  18. puts "deleted #{count} dangling #{model.to_s} records"
  19. end
  20. PaperTrail::Version.where(item_type: 'Motion').delete_all
  21. ActiveStorage::Blob.unattached.where("active_storage_blobs.created_at < ?", 7.days.ago).find_each(&:purge_later)
  22. # ["Comment", "Discussion", "Group", "Membership", "Outcome", "Poll", "Stance", "User"].each do |model|
  23. # table = model.pluralize.downcase
  24. # # puts PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").to_sql
  25. # # puts PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").count
  26. # count = PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").delete_all
  27. # puts "deleted #{count} dangling #{table} version records"
  28. # end
  29. # real delete of dangling active storage objects
  30. # delete subscription records where no group references them
  31. end
  32. end

app/services/comment_service.rb

85.71% lines covered

35 relevant lines. 30 lines covered and 5 lines missed.
    
  1. 1 class CommentService
  2. 1 def self.create(comment:, actor:)
  3. 188 actor.ability.authorize! :create, comment
  4. 187 comment.author = actor
  5. 187 return false unless comment.valid?
  6. 177 comment.save!
  7. 177 EventBus.broadcast('comment_create', comment, actor)
  8. 177 Events::NewComment.publish!(comment)
  9. end
  10. 1 def self.discard(comment:, actor:)
  11. 3 actor.ability.authorize!(:discard, comment)
  12. 2 ActiveRecord::Base.transaction do
  13. 2 comment.update(discarded_at: Time.now, discarded_by: actor.id)
  14. 2 comment.created_event.update(user_id: nil, pinned: false)
  15. end
  16. 2 comment.created_event
  17. end
  18. 1 def self.undiscard(comment:, actor:)
  19. actor.ability.authorize!(:undiscard, comment)
  20. ActiveRecord::Base.transaction do
  21. comment.update(discarded_at: nil, discarded_by: nil)
  22. comment.created_event.update(user_id: comment.user_id)
  23. end
  24. comment.created_event
  25. end
  26. 1 def self.destroy(comment:, actor:)
  27. 5 actor.ability.authorize!(:destroy, comment)
  28. 4 comment_id = comment.id
  29. 4 discussion_id = comment.discussion.id
  30. # you cannot delete a comment if it has replies
  31. # but if you could, you'd need to delete all the children, or rehome them
  32. # Comment.where(parent_id: comment.id, parent_type: 'Comment').destroy_all
  33. # Comment.where(parent_id: comment.id, parent_type: 'Comment').update(parent: comment.parent)
  34. 4 comment.destroy
  35. 4 RepairThreadWorker.perform_async(discussion_id)
  36. end
  37. 1 def self.update(comment:, params:, actor:)
  38. 10 actor.ability.authorize! :update, comment
  39. 7 comment.edited_at = Time.zone.now
  40. 7 comment.assign_attributes_and_files(params)
  41. 7 return false unless comment.valid?
  42. 5 comment.save!
  43. 5 comment.update_versions_count
  44. 5 EventBus.broadcast('comment_update', comment, actor)
  45. 5 Events::CommentEdited.publish!(comment, actor)
  46. end
  47. end

app/services/contact_message_service.rb

0.0% lines covered

19 relevant lines. 0 lines covered and 19 lines missed.
    
  1. class ContactMessageService
  2. def self.create(contact_message:, actor:)
  3. if contact_message.valid?
  4. ContactMailer.contact_message(
  5. contact_message.name,
  6. contact_message.email,
  7. contact_message.subject,
  8. contact_message.message,
  9. {
  10. site: ENV['CANONICAL_HOST'],
  11. form_type: 'Support',
  12. user_id: actor.id
  13. }.compact
  14. ).deliver_later
  15. else
  16. raise "failed to send a contact message. name: #{contact_message.name}, #{contact_message.email}, #{contact_message.subject}, #{contact_message.errors.to_s}"
  17. end
  18. end
  19. end

app/services/demo_service.rb

0.0% lines covered

42 relevant lines. 0 lines covered and 42 lines missed.
    
  1. class DemoService
  2. def self.refill_queue
  3. return unless ENV['FEATURES_DEMO_GROUPS']
  4. demo = Demo.where('demo_handle is not null').last
  5. return unless demo
  6. # precache translations
  7. AppConfig.locales['supported'].each do |locale|
  8. TranslationService.translate_group_content!(demo.group, locale, true)
  9. end
  10. expected = ENV.fetch('FEATURES_DEMO_GROUPS_SIZE', 3)
  11. remaining = Redis::List.new('demo_group_ids').value.size
  12. (expected - remaining).times do
  13. group = RecordCloner.new(recorded_at: demo.recorded_at).create_clone_group(demo.group)
  14. Redis::List.new('demo_group_ids').push(group.id)
  15. end
  16. end
  17. def self.take_demo(actor)
  18. group = Group.find(Redis::List.new('demo_group_ids').shift)
  19. group.creator = actor
  20. group.subscription = Subscription.new(plan: 'demo', owner: actor)
  21. group.add_member! actor
  22. group.save!
  23. if actor.locale != "en"
  24. TranslationService.translate_group_content!(group, actor.locale)
  25. end
  26. EventBus.broadcast('demo_started', actor)
  27. group
  28. end
  29. def self.ensure_queue
  30. return unless ENV['FEATURES_DEMO_GROUPS']
  31. existing_ids = Redis::List.new('demo_group_ids').value.select { |id| Group.where(id: id).exists? }
  32. Redis::List.new('demo_group_ids').clear
  33. Redis::List.new('demo_group_ids').unshift(*existing_ids) if existing_ids.any?
  34. refill_queue
  35. end
  36. def self.generate_demo_groups
  37. Demo.where("demo_handle IS NOT NULL").each do |template|
  38. Group.where(handle: template.demo_handle).update_all(handle: nil)
  39. RecordCloner.new(recorded_at: template.recorded_at)
  40. .create_clone_group_for_public_demo(template.group, template.demo_handle)
  41. end
  42. end
  43. end

app/services/discussion_reader_service.rb

66.67% lines covered

6 relevant lines. 4 lines covered and 2 lines missed.
    
  1. 1 class DiscussionReaderService
  2. 1 def self.redeem(discussion_reader: , actor: )
  3. 2 return unless DiscussionReader.redeemable_by(actor).where(id: discussion_reader.id).exists?
  4. 1 discussion_reader.update(user: actor, accepted_at: Time.zone.now)
  5. rescue ActiveRecord::RecordNotUnique
  6. DiscussionReader.find_by(discussion_id: discussion_reader.discussion_id,
  7. user_id: actor.id).
  8. update(inviter_id: discussion_reader.inviter_id,
  9. accepted_at: Time.zone.now)
  10. discussion_reader.destroy
  11. end
  12. end

app/services/discussion_service.rb

91.27% lines covered

126 relevant lines. 115 lines covered and 11 lines missed.
    
  1. 1 class DiscussionService
  2. 1 def self.create(discussion:, actor:, params: {})
  3. 371 actor.ability.authorize!(:create, discussion)
  4. 367 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  5. emails: params[:recipient_emails],
  6. audience: params[:recipient_audience],
  7. model: discussion,
  8. actor: actor)
  9. 365 discussion.author = actor
  10. 365 return false unless discussion.valid?
  11. 363 discussion.save!
  12. 363 DiscussionReader.for(user: actor, discussion: discussion)
  13. .update(admin: true, guest: !discussion.group.present?, inviter_id: actor.id)
  14. 363 users = add_users(user_ids: params[:recipient_user_ids],
  15. emails: params[:recipient_emails],
  16. audience: params[:recipient_audience],
  17. discussion: discussion,
  18. actor: actor)
  19. 363 EventBus.broadcast('discussion_create', discussion, actor)
  20. 363 Events::NewDiscussion.publish!(discussion: discussion,
  21. recipient_user_ids: users.pluck(:id),
  22. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  23. recipient_audience: params[:recipient_audience])
  24. end
  25. 1 def self.update(discussion:, actor:, params:)
  26. 13 actor.ability.authorize! :update, discussion
  27. 12 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  28. emails: params[:recipient_emails],
  29. audience: params[:recipient_audience],
  30. model: discussion,
  31. actor: actor)
  32. 12 discussion.assign_attributes_and_files(params.except(:group_id))
  33. 12 return false unless discussion.valid?
  34. 10 rearrange = discussion.max_depth_changed?
  35. 10 discussion.save!
  36. 10 discussion.update_versions_count
  37. 10 RepairThreadWorker.perform_async(discussion.id) if rearrange
  38. 10 users = add_users(discussion: discussion,
  39. actor: actor,
  40. user_ids: params[:recipient_user_ids],
  41. emails: params[:recipient_emails],
  42. audience: params[:recipient_audience])
  43. 10 EventBus.broadcast('discussion_update', discussion, actor, params)
  44. 10 Events::DiscussionEdited.publish!(discussion: discussion,
  45. actor: actor,
  46. recipient_user_ids: users.pluck(:id),
  47. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  48. recipient_audience: params[:recipient_audience],
  49. recipient_message: params[:recipient_message])
  50. end
  51. 1 def self.invite(discussion:, actor:, params:)
  52. 11 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  53. emails: params[:recipient_emails],
  54. audience: params[:recipient_audience],
  55. model: discussion,
  56. actor: actor)
  57. 9 users = add_users(discussion: discussion,
  58. actor: actor,
  59. user_ids: params[:recipient_user_ids],
  60. emails: params[:recipient_emails],
  61. audience: params[:recipient_audience])
  62. 9 Events::DiscussionAnnounced.publish!(discussion: discussion,
  63. actor: actor,
  64. recipient_user_ids: users.pluck(:id),
  65. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  66. recipient_audience: params[:recipient_audience],
  67. recipient_message: params[:recipient_message])
  68. end
  69. # def self.destroy(discussion:, actor:)
  70. # actor.ability.authorize!(:destroy, discussion)
  71. # discussion.discard!
  72. # DestroyDiscussionWorker.perform_async(discussion.id)
  73. # EventBus.broadcast('discussion_destroy', discussion, actor)
  74. # end
  75. 1 def self.discard(discussion:, actor:)
  76. actor.ability.authorize!(:discard, discussion)
  77. discussion.update(discarded_at: Time.now, discarded_by: actor.id)
  78. discussion.polls.update_all(discarded_at: Time.now, discarded_by: actor.id)
  79. GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
  80. EventBus.broadcast('discussion_discard', discussion, actor)
  81. discussion.created_event
  82. end
  83. 1 def self.close(discussion:, actor:)
  84. 3 actor.ability.authorize! :update, discussion
  85. 1 discussion.update(closed_at: Time.now, closer_id: actor.id)
  86. 1 MessageChannelService.publish_models([discussion], group_id: discussion.group_id, user_id: actor.id)
  87. end
  88. 1 def self.reopen(discussion:, actor:)
  89. 3 actor.ability.authorize! :update, discussion
  90. 1 discussion.update(closed_at: nil, closer_id: nil)
  91. 1 MessageChannelService.publish_models([discussion], group_id: discussion.group_id, user_id: actor.id)
  92. end
  93. 1 def self.move(discussion:, params:, actor:)
  94. 9 source = discussion.group
  95. 9 destination = ModelLocator.new(:group, params).locate || NullGroup.new
  96. 9 destination.present? && actor.ability.authorize!(:move_discussions_to, destination)
  97. 8 actor.ability.authorize! :move, discussion
  98. # discussion.add_admin!(actor)
  99. 7 discussion.update group: destination.presence, private: moved_discussion_privacy_for(discussion, destination)
  100. 8 discussion.polls.each { |poll| poll.update(group: destination.presence) }
  101. 7 ActiveStorage::Attachment.where(record: discussion.items.map(&:eventable).concat([discussion])).update_all(group_id: destination.id)
  102. 7 GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
  103. 7 EventBus.broadcast('discussion_move', discussion, params, actor)
  104. 7 Events::DiscussionMoved.publish!(discussion, actor, source)
  105. end
  106. 1 def self.pin(discussion:, actor:)
  107. 3 actor.ability.authorize! :pin, discussion
  108. 1 discussion.update(pinned_at: Time.now)
  109. 1 EventBus.broadcast('discussion_pin', discussion, actor)
  110. end
  111. 1 def self.unpin(discussion:, actor:)
  112. 1 actor.ability.authorize! :pin, discussion
  113. 1 discussion.update(pinned_at: nil)
  114. 1 EventBus.broadcast('discussion_pin', discussion, actor)
  115. end
  116. 1 def self.update_reader(discussion:, params:, actor:)
  117. 4 actor.ability.authorize! :show, discussion
  118. 2 reader = DiscussionReader.for(discussion: discussion, user: actor)
  119. 2 reader.update(params.slice(:volume))
  120. 2 Stance.joins(:poll).
  121. where('polls.discussion_id': reader.discussion_id).
  122. where(participant_id: actor.id).
  123. update(params.slice(:volume))
  124. 2 EventBus.broadcast('discussion_update_reader', reader, params, actor)
  125. end
  126. 1 def self.mark_as_seen(discussion:, actor:)
  127. 2 actor.ability.authorize! :mark_as_seen, discussion
  128. 1 reader = DiscussionReader.for_model(discussion, actor)
  129. 1 reader.viewed!
  130. 1 MessageChannelService.publish_models([reader.discussion], group_id: reader.discussion.group_id)
  131. 1 EventBus.broadcast('discussion_mark_as_seen', reader, actor)
  132. end
  133. 1 def self.mark_as_read_simple_params(discussion_id, ranges, actor_id)
  134. 2 discussion = Discussion.find(discussion_id)
  135. 2 actor = User.find(actor_id)
  136. 2 mark_as_read(discussion: discussion, params: {ranges: ranges}, actor: actor)
  137. end
  138. 1 def self.mark_as_read(discussion:, params:, actor:)
  139. 3 return unless actor.ability.can?(:mark_as_read, discussion)
  140. 3 RetryOnError.with_limit(2) do
  141. 3 sequence_ids = RangeSet.ranges_to_list(RangeSet.to_ranges(params[:ranges]))
  142. 3 NotificationService.viewed_events(actor_id: actor.id, discussion_id: discussion.id, sequence_ids: sequence_ids)
  143. 3 reader = DiscussionReader.for_model(discussion, actor)
  144. 3 reader.viewed!(params[:ranges])
  145. 3 EventBus.broadcast('discussion_mark_as_read', reader, actor)
  146. end
  147. end
  148. 1 def self.dismiss(discussion:, params:, actor:)
  149. 1 actor.ability.authorize! :dismiss, discussion
  150. 1 reader = DiscussionReader.for(user: actor, discussion: discussion)
  151. 1 reader.dismiss!
  152. 1 EventBus.broadcast('discussion_dismiss', reader, actor)
  153. end
  154. 1 def self.recall(discussion:, params:, actor:)
  155. 1 actor.ability.authorize! :dismiss, discussion
  156. 1 reader = DiscussionReader.for(user: actor, discussion: discussion)
  157. 1 reader.recall!
  158. 1 EventBus.broadcast('discussion_recall', reader, actor)
  159. end
  160. 1 def self.moved_discussion_privacy_for(discussion, destination)
  161. 7 case destination.discussion_privacy_options
  162. 1 when 'public_only' then false
  163. 5 when 'private_only' then true
  164. 1 else discussion.private
  165. end
  166. end
  167. 1 def self.mark_summary_email_as_read(user_id, time_start_i, time_finish_i)
  168. 1 user = User.find_by!(id: user_id)
  169. 1 time_start = Time.at(time_start_i).utc
  170. 1 time_finish = Time.at(time_finish_i).utc
  171. 1 time_range = time_start..time_finish
  172. 1 DiscussionQuery.visible_to(user: user, only_unread: true, or_public: false, or_subgroups: false).last_activity_after(time_start).each do |discussion|
  173. 1 RetryOnError.with_limit(2) do
  174. 1 sequence_ids = discussion.items.where("events.created_at": time_range).pluck(:sequence_id)
  175. 1 DiscussionReader.for(user: user, discussion: discussion).viewed!(sequence_ids)
  176. end
  177. end
  178. end
  179. 1 def self.add_users(discussion:, actor:, user_ids:, emails:, audience:)
  180. 410 users = UserInviter.where_or_create!(actor: actor,
  181. user_ids: user_ids,
  182. emails: emails,
  183. model: discussion,
  184. audience: audience)
  185. 410 volumes = {}
  186. 410 Membership.where(group_id: discussion.group_id,
  187. user_id: users.pluck(:id)).find_each do |m|
  188. 40 volumes[m.user_id] = m.volume
  189. end
  190. 410 DiscussionReader.
  191. where(discussion_id: discussion.id, user_id: users.map(&:id)).
  192. where("revoked_at is not null").update_all(revoked_at: nil, revoker_id: nil)
  193. 410 new_discussion_readers = users.map do |user|
  194. 47 DiscussionReader.new(user: user,
  195. discussion: discussion,
  196. inviter: actor,
  197. guest: !volumes.has_key?(user.id),
  198. admin: !discussion.group_id,
  199. volume: volumes[user.id] || user.default_membership_volume)
  200. end
  201. 410 DiscussionReader.import(new_discussion_readers, on_duplicate_key_ignore: true)
  202. 410 discussion.update_members_count
  203. 410 users
  204. end
  205. 1 def self.extract_link_preview_urls(discussion)
  206. urls = discussion.link_previews.map { |lp| lp['url'] }
  207. discussion.items.each do |event|
  208. if event.eventable.present? && event.eventable.respond_to?(:link_previews)
  209. urls.concat(event.eventable.link_previews.map {|lp| lp['url']})
  210. end
  211. end
  212. urls.compact.uniq
  213. end
  214. end

app/services/discussion_template_service.rb

0.0% lines covered

57 relevant lines. 0 lines covered and 57 lines missed.
    
  1. class DiscussionTemplateService
  2. def self.create(discussion_template:, actor:)
  3. actor.ability.authorize! :create, discussion_template
  4. discussion_template.assign_attributes(author: actor)
  5. return false unless discussion_template.valid?
  6. if discussion_template.key
  7. discussion_template.group.hidden_discussion_templates += Array(discussion_template.key)
  8. discussion_template.key = nil
  9. end
  10. discussion_template.save!
  11. discussion_template
  12. end
  13. def self.update(discussion_template:, params:, actor:)
  14. actor.ability.authorize! :update, discussion_template
  15. discussion_template.assign_attributes_and_files(params.except(:group_id))
  16. return false unless discussion_template.valid?
  17. discussion_template.save!
  18. discussion_template
  19. end
  20. def self.initial_templates(category)
  21. names = {
  22. board: ['discuss_a_topic', 'onboarding_to_loomio', 'approve_a_document', 'prepare_for_a_meeting', 'funding_decision'],
  23. membership: ['discuss_a_topic', 'onboarding_to_loomio', 'share_links_and_info', 'decision_by_consensus', 'elect_a_governance_position'],
  24. self_managing: ['discuss_a_topic', 'onboarding_to_loomio', 'advice_process', 'consent_process'],
  25. other: ['discuss_a_topic', 'onboarding_to_loomio', 'approve_a_document', 'advice_process', 'consent_process'],
  26. }.with_indifferent_access.fetch(category, ['blank'])
  27. default_templates.filter { |dt| names.include? dt.key }
  28. end
  29. def self.default_templates
  30. AppConfig.discussion_templates.map do |key, raw_attrs|
  31. raw_attrs[:key] = key
  32. attrs = {}
  33. raw_attrs.each_pair do |key, value|
  34. if key.match /_i18n$/
  35. attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
  36. else
  37. attrs[key] = value
  38. end
  39. end
  40. DiscussionTemplate.new attrs
  41. end.reverse
  42. end
  43. def self.create_public_templates
  44. group = Group.find_or_create_by(handle: 'templates') do |group|
  45. group.creator = User.helper_bot
  46. group.name = 'Loomio Templates'
  47. group.is_visible_to_public = false
  48. group.logo.attach(io: URI.open(Rails.root.join('public/brand/icon_gold_256h.png')),
  49. filename: 'loomiologo.png')
  50. end
  51. group.discussion_templates = default_templates.map do |dt|
  52. dt.public = true
  53. dt.author = User.helper_bot
  54. dt
  55. end
  56. end
  57. end

app/services/document_service.rb

0.0% lines covered

22 relevant lines. 0 lines covered and 22 lines missed.
    
  1. class DocumentService
  2. def self.create(document:, actor:)
  3. actor.ability.authorize! :create, document
  4. document.assign_attributes(author: actor)
  5. document.title ||= document.file_file_name
  6. return unless document.valid?
  7. document.save!
  8. EventBus.broadcast 'document_create', document, actor
  9. end
  10. def self.update(document:, params:, actor:)
  11. actor.ability.authorize! :update, document
  12. document.assign_attributes(params.slice(:url, :title, :model_id, :model_type))
  13. return unless document.valid?
  14. document.save!
  15. EventBus.broadcast 'document_update', document, params, actor
  16. end
  17. def self.destroy(document:, actor:)
  18. actor.ability.authorize! :destroy, document
  19. document.destroy
  20. EventBus.broadcast 'document_destroy', document, actor
  21. end
  22. end

app/services/event_service.rb

73.91% lines covered

46 relevant lines. 34 lines covered and 12 lines missed.
    
  1. 1 class EventService
  2. 1 def self.remove_from_thread(event:, actor:)
  3. discussion = event.discussion
  4. raise CanCan::AccessDenied.new unless event.kind == 'discussion_edited'
  5. actor.ability.authorize! :remove_events, discussion
  6. event.update(discussion_id: nil)
  7. discussion.thread_item_destroyed!
  8. GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
  9. EventBus.broadcast('event_remove_from_thread', event)
  10. event
  11. end
  12. 1 def self.move_comments(discussion:, actor:, params:)
  13. # handle parent comments = events where parent_id is source.created_event.id
  14. # move all events which are children of above parents (comment parent id untouched)
  15. # handle any reply comments that don't have parent_id in given ids
  16. 7 ids = Array(params[:forked_event_ids]).compact
  17. 7 source = Event.find(ids.first).discussion
  18. 7 actor.ability.authorize! :move_comments, source
  19. 6 actor.ability.authorize! :move_comments, discussion
  20. 5 MoveCommentsWorker.perform_async(ids, source.id, discussion.id)
  21. end
  22. 1 def self.repair_thread(discussion_id)
  23. 25 discussion = Discussion.find_by(id: discussion_id)
  24. 25 return unless discussion
  25. # ensure discussion.created_event exists
  26. 25 unless discussion.created_event
  27. Event.import [Event.new(kind: 'new_discussion',
  28. user_id: discussion.author_id,
  29. eventable_id: discussion.id,
  30. eventable_type: "Discussion",
  31. created_at: discussion.created_at)]
  32. discussion.reload
  33. end
  34. 25 Event.where(discussion_id: discussion_id, sequence_id: nil).order(:id).each(&:set_sequence_id!)
  35. # rebuild ancestry of events based on eventable relationships
  36. 25 items = Event.where(discussion_id: discussion.id).order(:sequence_id)
  37. 25 items.update_all(parent_id: discussion.created_event.id, position: 0, position_key: nil, depth: 1)
  38. 25 items.reload.compact.each(&:set_parent_and_depth!)
  39. 25 parent_ids = items.pluck(:parent_id).compact.uniq
  40. 25 reset_child_positions(discussion.created_event.id, nil)
  41. 25 Event.where(id: parent_ids).order(:depth).each do |parent_event|
  42. 37 parent_event.reload
  43. 37 reset_child_positions(parent_event.id, parent_event.position_key)
  44. end
  45. 25 ActiveRecord::Base.connection.execute(
  46. "UPDATE events
  47. SET descendant_count = (
  48. SELECT count(descendants.id)
  49. FROM events descendants
  50. WHERE
  51. descendants.discussion_id = events.discussion_id AND
  52. descendants.id != events.id AND
  53. descendants.position_key like CONCAT(events.position_key, '%')
  54. ), child_count = (
  55. SELECT count(children.id) FROM events children
  56. WHERE children.parent_id = events.id AND children.discussion_id IS NOT NULL
  57. )
  58. WHERE discussion_id = #{discussion_id.to_i}")
  59. 25 discussion.created_event.update_child_count
  60. 25 discussion.created_event.update_descendant_count
  61. 25 discussion.update_sequence_info!
  62. # ensure all the discussion_readers have valid read_ranges values
  63. 25 DiscussionReader.where(discussion_id: discussion_id).each do |reader|
  64. 41 reader.update_columns(
  65. read_ranges_string: RangeSet.serialize(
  66. RangeSet.intersect_ranges(reader.read_ranges, discussion.ranges)
  67. )
  68. )
  69. end
  70. end
  71. 1 def self.reset_child_positions(parent_id, parent_position_key)
  72. 74 position_key_sql = if parent_position_key.nil?
  73. 60 "CONCAT(REPEAT('0',5-LENGTH(CONCAT(t.seq))), t.seq)"
  74. else
  75. 14 "CONCAT('#{parent_position_key}-', CONCAT(REPEAT('0',5-LENGTH(CONCAT(t.seq) ) ), t.seq) )"
  76. end
  77. 74 ActiveRecord::Base.connection.execute(
  78. "UPDATE events SET position = t.seq, position_key = #{position_key_sql}
  79. FROM (
  80. SELECT id AS id, row_number() OVER(ORDER BY sequence_id) AS seq
  81. FROM events
  82. WHERE parent_id = #{parent_id}
  83. AND discussion_id IS NOT NULL
  84. ) AS t
  85. WHERE events.id = t.id and
  86. events.position is distinct from t.seq")
  87. 74 SequenceService.drop_seq!('events_position', parent_id)
  88. end
  89. 1 def self.repair_all_threads
  90. Discussion.pluck(:id).each do |id|
  91. RepairThreadWorker.perform_async(id)
  92. end
  93. end
  94. end

app/services/group_export_service.rb

27.62% lines covered

105 relevant lines. 29 lines covered and 76 lines missed.
    
  1. 1 class GroupExportService
  2. RELATIONS = %w[
  3. 1 all_users
  4. all_events
  5. all_notifications
  6. all_reactions
  7. poll_templates
  8. discussion_templates
  9. memberships
  10. membership_requests
  11. discussions
  12. exportable_polls
  13. exportable_poll_options
  14. exportable_outcomes
  15. exportable_stances
  16. exportable_stance_choices
  17. discussion_readers
  18. comments
  19. ]
  20. JSON_PARAMS = {
  21. 1 groups: {except: [:token, :secret_token], methods: []},
  22. comments: {except: [:secret_token]},
  23. discussions: {except: [:secret_token]},
  24. polls: {except: [:secret_token]},
  25. outcomes: {except: [:secret_token]},
  26. users: {except: [:encrypted_password,
  27. :reset_password_token,
  28. :email_api_key,
  29. :reset_password_token,
  30. :secret_token,
  31. :unsubscribe_token] }
  32. }.with_indifferent_access.freeze
  33. BACK_REFERENCES = {
  34. 1 outcomes: {
  35. events: %w[eventable]
  36. },
  37. comments: {
  38. comments: %w[parent_id],
  39. events: %w[eventable]
  40. },
  41. discussions: {
  42. comments: %w[discussion_id],
  43. discussion_readers: %w[discussion_id],
  44. polls: %w[discussion_id],
  45. events: %w[discussion_id eventable]
  46. },
  47. events: {
  48. events: %w[parent_id],
  49. notifications: %w[event_id]
  50. },
  51. groups: {
  52. memberships: %w[group_id],
  53. polls: %w[group_id],
  54. discussions: %w[group_id],
  55. tags: %w[group_id],
  56. webhooks: %w[group_id],
  57. events: %w[eventable],
  58. groups: %w[parent_id],
  59. poll_templates: %w[group_id],
  60. discussion_templates: %w[group_id]
  61. },
  62. poll_options: {
  63. stance_choices: %w[poll_option_id],
  64. events: %w[eventable]
  65. },
  66. stances: {
  67. stance_choices: %w[stance_id],
  68. events: %w[eventable]
  69. },
  70. tasks: {
  71. tasks_users: %w[task_id],
  72. events: %w[eventable]
  73. },
  74. polls: {
  75. stances: %w[poll_id],
  76. poll_options: %w[poll_id],
  77. outcomes: %w[poll_id],
  78. events: %w[eventable]
  79. },
  80. users: {
  81. events: %w[eventable user_id],
  82. discussions: %w[author_id discarded_by],
  83. attachments: %w[user_id],
  84. comments: %w[user_id discarded_by] ,
  85. discussion_readers: %w[user_id inviter_id],
  86. groups: %w[creator_id],
  87. membership_requests: %w[requestor_id responder_id],
  88. memberships: %w[user_id inviter_id],
  89. notifications: %w[user_id],
  90. outcomes: %w[author_id],
  91. polls: %w[author_id discarded_by],
  92. reactions: %w[user_id],
  93. stances: %w[participant_id inviter_id],
  94. subscriptions: %w[owner_id],
  95. tasks: %w[doer_id author_id],
  96. tasks_users: %w[user_id],
  97. versions: %w[whodunnit],
  98. webhooks: %w[author_id]
  99. }
  100. }.with_indifferent_access.freeze
  101. # export all the direct (invite-only) threads that people in a group have made
  102. # TODO make this part of a normal export group process
  103. 1 def self.export_direct_threads(group_id)
  104. group = Group.find(group_id)
  105. group_ids = Group.find(group_id).id_and_subgroup_ids
  106. author_ids = Membership.where(group_id: group_ids).pluck(:user_id).uniq
  107. discussion_ids = Discussion.where(group_id: nil, author_id: author_ids).pluck(:id)
  108. filename = "/tmp/#{DateTime.now.strftime("%Y-%m-%d_%H-%M-%S")}_invite-only-threads-for-#{group.name.parameterize}.json"
  109. ids = Hash.new { |hash, key| hash[key] = [] }
  110. File.open(filename, 'w') do |file|
  111. Discussion.where(id: discussion_ids).each do |discussion|
  112. puts_record(discussion, file, ids)
  113. %w[exportable_polls
  114. exportable_poll_options
  115. exportable_outcomes
  116. exportable_stances
  117. exportable_stance_choices
  118. all_reactions
  119. comments
  120. readers
  121. items
  122. discussion_readers].each do |relation|
  123. discussion.send(relation).find_each(batch_size: 20000) do |record|
  124. puts_record(record, file, ids)
  125. end
  126. end
  127. attachments = [
  128. discussion.files,
  129. discussion.image_files,
  130. discussion.comment_files,
  131. discussion.comment_image_files,
  132. discussion.poll_files,
  133. discussion.poll_image_files,
  134. discussion.outcome_files,
  135. discussion.outcome_image_files
  136. ].compact.flatten.uniq.each do |attachment|
  137. puts_attachment(attachment, file)
  138. end
  139. end
  140. end
  141. filename
  142. end
  143. 1 def self.export(groups, group_name)
  144. 1 filename = export_filename_for(group_name)
  145. 4 ids = Hash.new { |hash, key| hash[key] = [] }
  146. 1 File.open(filename, 'w') do |file|
  147. 1 groups.each do |group|
  148. 1 puts_record(group, file, ids)
  149. 1 RELATIONS.each do |relation|
  150. # puts "Exporting: #{relation}"
  151. 16 group.send(relation).find_each(batch_size: 20000) do |record|
  152. 4 puts_record(record, file, ids)
  153. end
  154. end
  155. 1 user_attachments = group.all_users.map(&:uploaded_avatar_attachment)
  156. 1 own_attachments = [group.cover_photo_attachment,
  157. group.logo_attachment,
  158. group.files_attachments,
  159. group.image_files_attachments]
  160. 1 related_attachments = [group.comment_files,
  161. group.comment_image_files,
  162. group.discussion_files,
  163. group.discussion_image_files,
  164. group.poll_files,
  165. group.poll_image_files,
  166. group.outcome_files,
  167. group.outcome_image_files,
  168. group.subgroup_files,
  169. group.subgroup_image_files,
  170. group.subgroup_cover_photos,
  171. group.subgroup_logos]
  172. 1 (user_attachments + own_attachments + related_attachments).
  173. compact.flatten.uniq.each do |attachment|
  174. puts_attachment(attachment, file)
  175. end
  176. end
  177. end
  178. 1 filename
  179. end
  180. 1 def self.export_filename_for(group_name)
  181. 1 "/tmp/#{DateTime.now.strftime("%Y-%m-%d_%H-%M-%S")}_#{group_name.parameterize}.json"
  182. end
  183. 1 def self.puts_attachment(attachment, file)
  184. download_path = Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
  185. obj = {
  186. id: attachment.id,
  187. host: ENV['CANONICAL_HOST'],
  188. record_type: attachment.record_type,
  189. record_id: attachment.record_id,
  190. name: attachment.name,
  191. filename: attachment.filename,
  192. content_type: attachment.content_type,
  193. path: download_path,
  194. url: "https://#{ENV['CANONICAL_HOST']}#{download_path}"
  195. }
  196. file.puts({table: 'attachments', record: obj}.to_json)
  197. end
  198. 1 def self.puts_record(record, file, ids)
  199. 5 table = record.class.table_name
  200. 5 return if ids[table].include?(record.id)
  201. 5 ids[table] << record.id
  202. 5 file.puts({table: table, record: record.as_json(JSON_PARAMS[table])}.to_json)
  203. end
  204. 1 def self.import(filename_or_url, reset_keys: false)
  205. group_ids = []
  206. migrate_ids = {}
  207. if URI.parse(filename_or_url).class == URI::Generic
  208. datas = File.open(filename_or_url).read.split("\n").map { |line| JSON.parse(line) }
  209. else
  210. datas = URI.parse(filename_or_url).read.split("\n").map { |line| JSON.parse(line) }
  211. end
  212. tables = datas.map{ |data| data['table'] }.uniq
  213. ActiveRecord::Base.transaction do
  214. #import the records, remember old with new ids
  215. (tables - ['attachments']).each do |table|
  216. migrate_ids[table] = {}
  217. klass = table.classify.constantize
  218. datas.each do |data|
  219. next unless (data['table'] == table)
  220. record = klass.new(data['record'])
  221. if reset_keys && data['record'].has_key?('key')
  222. record.key = nil
  223. record.set_key
  224. end
  225. if data['record'].has_key?('secret_token')
  226. record.secret_token = nil
  227. end
  228. if data['record'].has_key?('token')
  229. record.token = klass.generate_unique_secure_token
  230. end
  231. if table == 'groups'
  232. record.handle = GroupService.suggest_handle(name: record.handle, parent_handle: nil)
  233. end
  234. old_id = record.id
  235. record.id = nil
  236. result = klass.import([record], validate: false, on_duplicate_key_ignore: true)
  237. if new_id = result.ids.map(&:to_i).first
  238. migrate_ids[table][old_id] = new_id
  239. else
  240. # duplicate record exists
  241. if table == 'users'
  242. migrate_ids[table][old_id] = User.find_by(email: record.email).id
  243. else
  244. raise "failed to import #{table} record - conflict on unique column. handle that here"
  245. end
  246. end
  247. end
  248. end
  249. # SIDEKIQ_REDIS_POOL.with_client do |client|
  250. # client.set "last_migrate_ids", migrate_ids.to_json
  251. # end
  252. # rewrite references to old ids
  253. (tables - ['attachments']).each do |table|
  254. migrate_ids[table].each_pair do |old_id, new_id|
  255. next unless BACK_REFERENCES.has_key?(table)
  256. BACK_REFERENCES[table].each_pair do |ref_table, columns|
  257. next unless migrate_ids[ref_table].present?
  258. imported_ids = migrate_ids[ref_table].values
  259. columns.each do |column|
  260. if column == "eventable"
  261. ref_table.classify.constantize.
  262. where(id: imported_ids).
  263. where(column+"_type" => table.classify, column+"_id" => old_id).
  264. update_all(column+"_id" => new_id)
  265. else
  266. ref_table.classify.constantize.
  267. where(id: imported_ids).
  268. where(column => old_id).
  269. update_all(column => new_id)
  270. end
  271. end
  272. end
  273. end
  274. end
  275. if tables.include?('attachments')
  276. datas.each do |data|
  277. next unless (data['table'] == 'attachments')
  278. table = data['record']['record_type'].tableize
  279. new_id = migrate_ids[table][data['record']['record_id']]
  280. DownloadAttachmentWorker.perform_async(data['record'], new_id)
  281. end
  282. end
  283. datas.each do |data|
  284. if data['table'] == 'polls'
  285. new_id = migrate_ids['polls'][data['record']['id']]
  286. Poll.find(new_id).update_counts!
  287. Poll.find(new_id).stances.each(&:update_option_scores!)
  288. end
  289. end
  290. end
  291. # SearchIndexWorker.new.perform(Discussion.where(group_id: group_ids).pluck(:id))
  292. end
  293. 1 def self.download_attachment(record_data, new_id)
  294. model = record_data['record_type'].classify.constantize.find(new_id)
  295. file = URI.open(record_data['url'])
  296. model.send(record_data['name']).attach(io: file, filename: record_data['filename'])
  297. if model.respond_to?(:attachments)
  298. model.update_attribute(:attachments, model.build_attachments)
  299. end
  300. file.close
  301. end
  302. end

app/services/group_service.rb

97.67% lines covered

86 relevant lines. 84 lines covered and 2 lines missed.
    
  1. 1 module GroupService
  2. 1 def self.remote_cover_photo
  3. # id like to use unsplash api but need to work out how to meet their attribution requirements
  4. filename = %w[
  5. 8 cover1.jpg
  6. cover2.jpg
  7. cover3.jpg
  8. cover4.jpg
  9. cover5.jpg
  10. cover6.jpg
  11. cover7.jpg
  12. cover8.jpg
  13. cover9.jpg
  14. cover10.jpg
  15. cover11.jpg
  16. cover12.jpg
  17. cover13.jpg
  18. cover14.jpg
  19. ].sample
  20. 8 Rails.root.join("public/theme/group_cover_photos/#{filename}")
  21. end
  22. 1 def self.invite(group:, params:, actor:)
  23. 17 group_ids = if params[:invited_group_ids]
  24. 2 Array(params[:invited_group_ids]).map(&:to_i)
  25. else
  26. 15 Array(group.id)
  27. end
  28. # restrict group_ids to a single organization
  29. 17 parent_group = Group.where(id: group_ids).first.parent_or_self
  30. 17 group_ids = parent_group.id_and_subgroup_ids & group_ids
  31. 17 UserInviter.authorize_add_members!(
  32. parent_group: parent_group,
  33. group_ids: group_ids,
  34. emails: Array(params[:recipient_emails]),
  35. user_ids: Array(params[:recipient_user_ids]),
  36. actor: actor,
  37. )
  38. 13 users = UserInviter.where_or_create!(
  39. actor: actor,
  40. model: group,
  41. emails: params[:recipient_emails],
  42. user_ids: params[:recipient_user_ids]
  43. )
  44. 13 Group.where(id: group_ids).each do |g|
  45. 14 revoked_memberships = Membership.revoked.where(group_id: g.id, user_id: users.map(&:id))
  46. 14 revoked_memberships.update_all(
  47. inviter_id: actor.id,
  48. accepted_at: nil,
  49. revoked_at: nil,
  50. revoker_id: nil,
  51. admin: false,
  52. )
  53. 14 new_memberships = users.map do |user|
  54. 12 Membership.new(inviter: actor, user: user, group: g, volume: user.default_membership_volume)
  55. end
  56. 14 Membership.import(new_memberships, on_duplicate_key_ignore: true)
  57. # mark as accepted all invitiations to people who are already part of the org.
  58. 14 if g.parent
  59. 5 parent_members = g.parent.accepted_members.where(id: users.verified.pluck(:id))
  60. 5 Membership.pending.where(group_id: g.id,
  61. user_id: parent_members.pluck(:id)).update_all(accepted_at: Time.now)
  62. end
  63. 14 g.update_pending_memberships_count
  64. 14 g.update_memberships_count
  65. 14 GenericWorker.perform_async('PollService', 'group_members_added', g.id)
  66. end
  67. 13 Events::MembershipCreated.publish!(
  68. group: group,
  69. actor: actor,
  70. recipient_user_ids: users.pluck(:id),
  71. recipient_message: params[:recipient_message]
  72. )
  73. 13 Membership.active.where(group_id: group.id, user_id: users.pluck(:id))
  74. end
  75. 1 def self.create(group:, actor: , skip_authorize: false)
  76. 8 actor.ability.authorize!(:create, group) unless skip_authorize
  77. 8 return false unless group.valid?
  78. 8 group.is_referral = actor.groups.size > 0
  79. 8 if group.is_parent?
  80. 8 url = remote_cover_photo
  81. 8 group.cover_photo.attach(io: URI.open(url), filename: File.basename(url))
  82. 8 group.creator = actor if actor.is_logged_in?
  83. 8 group.subscription = Subscription.new
  84. end
  85. 8 group.save!
  86. 8 group.add_admin!(actor)
  87. 8 EventBus.broadcast('group_create', group, actor)
  88. end
  89. 1 def self.update(group:, params:, actor:)
  90. 4 actor.ability.authorize! :update, group
  91. 4 group.assign_attributes_and_files(params)
  92. 4 group.group_privacy = params[:group_privacy] if params.has_key?(:group_privacy)
  93. 4 privacy_change = PrivacyChange.new(group)
  94. 4 return false unless group.valid?
  95. 4 group.save!
  96. 4 privacy_change.commit!
  97. 4 EventBus.broadcast('group_update', group, params, actor)
  98. end
  99. 1 def self.destroy(group:, actor:)
  100. 3 actor.ability.authorize! :destroy, group
  101. 1 group.admins.each do |admin|
  102. 2 GroupMailer.destroy_warning(group.id, admin.id, actor.id).deliver_later
  103. end
  104. 1 group.archive!
  105. 1 DestroyGroupWorker.perform_in(2.weeks, group.id)
  106. 1 EventBus.broadcast('group_destroy', group, actor)
  107. end
  108. 1 def self.destroy_without_warning!(group_id)
  109. Group.find(group_id).archive!
  110. DestroyGroupWorker.perform_async(group_id)
  111. end
  112. 1 def self.move(group:, parent:, actor:)
  113. 2 actor.ability.authorize! :move, group
  114. 1 group.update(handle: "#{parent.handle}-#{group.handle}") if group.handle?
  115. 1 group.update(parent: parent, subscription_id: nil)
  116. 1 EventBus.broadcast('group_move', group, parent, actor)
  117. end
  118. 1 def self.export(group: , actor: )
  119. 1 actor.ability.authorize! :show, group
  120. 1 group_ids = actor.groups.where(id: group.all_groups).pluck(:id)
  121. 1 GroupExportWorker.perform_async(group_ids, group.name, actor.id)
  122. end
  123. 1 def self.merge(source:, target:, actor:)
  124. 2 actor.ability.authorize! :merge, source
  125. 1 actor.ability.authorize! :merge, target
  126. 1 Group.transaction do
  127. 1 source.subgroups.update_all(parent_id: target.id)
  128. 1 source.discussions.update_all(group_id: target.id)
  129. 1 source.polls.update_all(group_id: target.id)
  130. 1 source.membership_requests.update_all(group_id: target.id)
  131. 1 source.group_identities.update_all(group_id: target.id)
  132. 1 source.memberships.where.not(user_id: target.member_ids).update_all(group_id: target.id)
  133. 1 source.destroy
  134. end
  135. end
  136. 1 def self.suggest_handle(name:, parent_handle:)
  137. 1321 attempt = 0
  138. 2643 while(Group.where(handle: generate_handle(name, parent_handle, attempt)).exists?) do
  139. 1 attempt += 1
  140. end
  141. 1321 generate_handle(name, parent_handle, attempt)
  142. end
  143. 1 private
  144. 1 def self.generate_handle(name, parent_handle, attempt)
  145. 2643 [parent_handle,
  146. name,
  147. 3017 (attempt == 0) ? nil : attempt].compact.map{|t| t.to_s.strip.parameterize}.join('-')
  148. end
  149. end

app/services/group_service/privacy_change.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. 1 class GroupService::PrivacyChange
  2. 1 attr_accessor :group
  3. 1 def initialize(group)
  4. 8 @group = group
  5. 8 @changed = group.changed
  6. end
  7. 1 def commit!
  8. 8 @changed.each do |attribute|
  9. 14 case attribute
  10. when 'is_visible_to_public'
  11. 4 if group.is_hidden_from_public?
  12. 2 make_discussions_private_in(group)
  13. 2 make_discussions_private_in(group.subgroups)
  14. 2 group.subgroups.each do |subgroup|
  15. 4 subgroup.group_privacy = 'closed'
  16. 4 subgroup.save!
  17. end
  18. end
  19. when 'discussion_privacy_options'
  20. 4 case group.discussion_privacy_options
  21. 1 when 'private_only' then make_discussions_private_in(group)
  22. 3 when 'public_only' then make_discussions_public_in(group)
  23. end
  24. end
  25. end
  26. end
  27. 1 private
  28. 1 def make_discussions_private_in(group_or_groups)
  29. 5 Discussion.where(group_id: group_or_groups).update_all(private: true)
  30. 5 Array(group_or_groups).map(&:update_public_discussions_count)
  31. end
  32. 1 def make_discussions_public_in(group_or_groups)
  33. 3 Discussion.where(group_id: group_or_groups).update_all(private: false)
  34. 3 Array(group_or_groups).map(&:update_public_discussions_count)
  35. end
  36. end

app/services/link_preview_service.rb

0.0% lines covered

39 relevant lines. 0 lines covered and 39 lines missed.
    
  1. module LinkPreviewService
  2. def self.fetch(url)
  3. # require logged in user
  4. # add rate limit of 100 per hour per user
  5. response = HTTParty.get(url)
  6. return nil if response.code != 200
  7. doc = Nokogiri::HTML::Document.parse(response.body)
  8. title = [doc.css('meta[property="og:title"]').attr('content')&.text,
  9. doc.css('title').first&.text,
  10. doc.css('h1').first&.text].reject(&:blank?).first
  11. bad_titles = [/Google \w+: Sign-in/]
  12. return nil if title.blank?
  13. return nil if bad_titles.any? {|bt| bt.match?(title) }
  14. description = [doc.css('meta[property="og:description"]').attr('content')&.text,
  15. doc.css('meta[name="description"]').attr('content')&.text].reject(&:blank?).first
  16. image = [doc.css('meta[property="og:image"]').attr('content')&.text,
  17. doc.css('meta[name="og:image"]').attr('content')&.text,
  18. doc.css('img[itemprop="image"]').attr('src')&.text,
  19. doc.css('link[rel="image_src"]').attr('href')&.text].reject(&:blank?).first
  20. {title: String(title).truncate(240),
  21. description: String(description).truncate(240),
  22. image: image,
  23. url: url,
  24. fit: 'contain',
  25. align: 'center',
  26. hostname: URI(url).host}
  27. end
  28. def self.fetch_urls(urls)
  29. previews = []
  30. threads = []
  31. Array(urls).compact.reject {|u| BlockedDomain.where(name: URI(u).host).exists? }.each do |u|
  32. # spawn a new thread for each url
  33. threads << Thread.new do
  34. previews.push fetch(u)
  35. end
  36. end
  37. threads.each { |t| t.join }
  38. previews.compact
  39. rescue SocketError, URI::InvalidURIError, HTTParty::UnsupportedURIScheme, HTTParty::RedirectionTooDeep
  40. []
  41. end
  42. end

app/services/login_token_service.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class LoginTokenService
  2. 1 def self.create(actor:, uri:)
  3. 12 return unless actor.presence
  4. 11 token = LoginToken.create!(redirect: (uri.path if uri&.host == ENV['CANONICAL_HOST']), user: actor)
  5. 11 UserMailer.login(actor.id, token.id).deliver_now
  6. 11 EventBus.broadcast('login_token_create', token, actor)
  7. end
  8. end

app/services/markdown_service.rb

77.08% lines covered

48 relevant lines. 37 lines covered and 11 lines missed.
    
  1. 1 module MarkdownService
  2. MARKDOWN_OPTIONS = [
  3. 1 no_intra_emphasis: true,
  4. tables: true,
  5. fenced_code_blocks: true,
  6. autolink: true,
  7. strikethrough: true,
  8. space_after_headers: true,
  9. superscript: true,
  10. underline: true
  11. ].freeze
  12. 1 def self.render_markdown(text, format = 'md')
  13. 41 text.gsub!('](/rails/active_storage', ']('+lmo_asset_host+'/rails/active_storage')
  14. 41 text.gsub!('"/rails/active_storage', '"'+lmo_asset_host+'/rails/active_storage')
  15. 41 if format == "md"
  16. 41 text
  17. else
  18. ReverseMarkdown.convert(text)
  19. end
  20. end
  21. 1 def self.render_html(text)
  22. 2825 return '' if text.nil?
  23. 2821 renderer = LoomioMarkdown.new(filter_html: true, hard_wrap: true, link_attributes: {rel: "nofollow ugc noreferrer noopener", target: :_blank})
  24. 2821 Redcarpet::Markdown.new(renderer, *MARKDOWN_OPTIONS).render(text)
  25. end
  26. 1 def self.render_rich_text(text, format = "md")
  27. 2485 return "" unless text
  28. 2485 text.gsub!('](/rails/active_storage', ']('+lmo_asset_host+'/rails/active_storage')
  29. 2485 text.gsub!('"/rails/active_storage', '"'+lmo_asset_host+'/rails/active_storage')
  30. 2485 if format == "md"
  31. 2440 MarkdownService.render_html(text)
  32. else
  33. 45 replace_audios(replace_videos(replace_checkboxes(replace_iframes(text))))
  34. end.html_safe
  35. end
  36. 1 def self.render_plain_text(text, format = 'md')
  37. 15 return "" unless text
  38. 15 ActionController::Base.helpers.strip_tags(render_rich_text(text, format)).gsub(/(?:\n\r?|\r\n?)/, '<br>')
  39. end
  40. 1 def self.replace_videos(str)
  41. 45 doc = Nokogiri::HTML5::DocumentFragment.parse(str)
  42. 45 doc.search("video[src]").each do |node|
  43. node.replace("<p><a href='#{node['src']}'><img src='#{node['poster']}'><br>#{I18n.t('record_modal.watch_video')}</a></p>")
  44. end
  45. 45 doc.to_s
  46. end
  47. 1 def self.replace_audios(str)
  48. 45 doc = Nokogiri::HTML5::DocumentFragment.parse(str)
  49. 45 doc.search("audio[src]").each do |node|
  50. node.replace("<p><a href='#{node['src']}'>#{I18n.t('record_modal.listen_to_audio')}</a></p>")
  51. end
  52. 45 doc.to_s
  53. end
  54. 1 def self.replace_iframes(str)
  55. 45 doc = Nokogiri::HTML5::DocumentFragment.parse(str)
  56. 45 doc.search("iframe[src]").each do |node|
  57. begin
  58. vi = VideoInfo.new(node['src'])
  59. node.replace("<div><a href='#{vi.url}'><img src='#{vi.thumbnail}' /></a></div>")
  60. rescue
  61. node.replace("<a href='#{node['src']}'>#{node['src']}</a>")
  62. end
  63. end
  64. 45 doc.to_s
  65. end
  66. 1 def self.replace_checkboxes(str)
  67. 45 frag = Nokogiri::HTML::DocumentFragment.parse(str)
  68. 45 frag.css('li[data-type="taskItem"]').each do |node|
  69. if node['data-checked'] == 'true'
  70. node.prepend_child '<div class="email-checkbox">✔️</div>'
  71. else
  72. node.prepend_child '<div class="email-checkbox">&nbsp;</div>'
  73. end
  74. if node['data-due-on']
  75. node.add_child '<span class="mailer-tag">📅 '+node['data-due-on']+'</div>'
  76. end
  77. end
  78. 45 frag.to_s
  79. end
  80. end

app/services/membership_request_service.rb

64.29% lines covered

14 relevant lines. 9 lines covered and 5 lines missed.
    
  1. 1 class MembershipRequestService
  2. 1 def self.create(membership_request:, actor:)
  3. membership_request.requestor = actor
  4. return false unless membership_request.valid?
  5. actor.ability.authorize!(:create, membership_request)
  6. membership_request.save!
  7. Events::MembershipRequested.publish!(membership_request)
  8. end
  9. 1 def self.approve(membership_request:, actor: )
  10. 2 actor.ability.authorize! :approve, membership_request
  11. 1 membership_request.approve!(actor)
  12. 1 Events::MembershipRequestApproved.publish!(membership_request.convert_to_membership!, actor)
  13. end
  14. 1 def self.ignore(membership_request: , actor: )
  15. 2 actor.ability.authorize! :ignore, membership_request
  16. 1 membership_request.ignore!(actor)
  17. end
  18. end

app/services/membership_service.rb

86.21% lines covered

87 relevant lines. 75 lines covered and 12 lines missed.
    
  1. 1 class MembershipService
  2. 1 def self.redeem_if_pending!(membership)
  3. 5 redeem(membership: membership, actor: membership.user) if membership && membership.accepted_at.nil?
  4. end
  5. 1 def self.redeem(membership:, actor:, notify: true)
  6. 11 raise Membership::InvitationAlreadyUsed.new(membership) if membership.accepted_at
  7. # so we want to accept all the pending invitations this person has been sent within this org
  8. # and we dont want any surprises if they already have some memberships.
  9. # they may be accepting memberships send to a different email (unverified_user)
  10. 11 accepted_at = DateTime.now
  11. 11 invited_group_id = membership.group_id
  12. 11 existing_group_ids = Membership.where(user_id: actor.id).pluck(:group_id)
  13. 11 existing_accepted_group_ids = Membership.active.accepted.where(user_id: actor.id).pluck(:group_id)
  14. 11 invited_group_ids = Membership.pending.where(user_id: membership.user_id, group_id: membership.group.parent_or_self.id_and_subgroup_ids).pluck(:group_id)
  15. # unrevoke any memberships the actor was just invited to
  16. 11 Membership.revoked
  17. .where(user_id: actor.id, group_id: invited_group_ids)
  18. .update(revoked_at: nil, revoker_id: nil, inviter_id: membership.inviter_id, accepted_at: accepted_at)
  19. # ensure actor has accepted any existing pending memberships to this group
  20. 11 Membership.pending
  21. .where(user_id: actor.id, group_id: invited_group_ids)
  22. .update(accepted_at: accepted_at)
  23. 11 Membership.pending
  24. 11 .where(user_id: membership.user_id, group_id: (invited_group_ids - existing_group_ids))
  25. .update(user_id: actor.id, accepted_at: accepted_at)
  26. 11 if (membership.user_id != actor.id)
  27. 8 Membership.where(user_id: membership.user_id, group_id: invited_group_ids).destroy_all
  28. end
  29. 11 invited_group_ids.each do |group_id|
  30. 12 GenericWorker.perform_async('PollService', 'group_members_added', group_id)
  31. end
  32. # remove any existing guest access in these groups
  33. 11 DiscussionReader.joins(:discussion)
  34. .where(user_id: actor.id, 'discussions.group_id': invited_group_ids, guest: true)
  35. .update_all(guest: false, revoked_at: nil, revoker_id: nil)
  36. 11 Stance.joins(:poll)
  37. .where(participant_id: actor.id, 'polls.group_id': invited_group_ids)
  38. .update_all(guest: false)
  39. # unrevoke any votes on active polls
  40. 11 Stance.joins(:poll)
  41. .where(participant_id: actor.id)
  42. .where('polls.group_id': invited_group_ids)
  43. .where('stances.revoked_at is not null')
  44. .where('polls.closed_at is null')
  45. .update_all(revoked_at: nil, revoker_id: nil)
  46. 11 return if existing_accepted_group_ids.include?(invited_group_id)
  47. 9 membership = Membership.find_by!(group_id: invited_group_id, user_id: actor.id)
  48. 9 Events::InvitationAccepted.publish!(membership) if notify && membership.accepted_at
  49. end
  50. 1 def self.revoke(membership:, actor:, revoked_at: DateTime.now)
  51. 4 actor.ability.authorize! :revoke, membership
  52. # revoke guest access in case they were a guest before they were a member and it was not already cleaned up by redeem
  53. 4 revoke_by_id(
  54. membership.group.id_and_subgroup_ids,
  55. membership.user_id,
  56. actor.id,
  57. revoked_at,
  58. )
  59. 4 EventBus.broadcast('membership_destroy', membership, actor)
  60. end
  61. 1 def self.revoke_by_id(group_ids, user_id, actor_id, revoked_at = DateTime.now)
  62. 13 DiscussionReader
  63. .joins(:discussion).guests
  64. .where('discussions.group_id': group_ids, user_id: user_id)
  65. .update_all(guest: false)
  66. 13 Stance.joins(:poll).guests
  67. .where('polls.group_id': group_ids, participant_id: user_id)
  68. .update_all(guest: false)
  69. # remove them from active polls
  70. 13 group_ids.each do |group_id|
  71. 10 PollService.group_members_removed(group_id, user_id, actor_id, revoked_at)
  72. end
  73. # revoke the membership
  74. 13 Membership.active
  75. .where(user_id: user_id, group_id: group_ids)
  76. .update_all(revoked_at: revoked_at, revoker_id: actor_id)
  77. 13 Group.where(id: group_ids).map(&:update_memberships_count)
  78. end
  79. 1 def self.update(membership:, params:, actor:)
  80. 1 actor.ability.authorize! :update, membership
  81. 1 membership.assign_attributes(params.slice(:title))
  82. 1 return false unless membership.valid?
  83. 1 membership.save!
  84. 1 update_user_titles_and_broadcast(membership.id)
  85. 1 EventBus.broadcast 'membership_update', membership, params, actor
  86. end
  87. 1 def self.update_user_titles_and_broadcast(membership_id)
  88. 1 membership = Membership.find(membership_id)
  89. 1 user = membership.user
  90. 1 group = membership.group
  91. 1 return unless user && group
  92. 1 titles = (user.experiences['titles'] || {})
  93. 1 titles[group.id] = membership.title
  94. 1 user.experiences['titles'] = titles
  95. 1 user.save!
  96. 1 MessageChannelService.publish_models([user], serializer: AuthorSerializer, group_id: group.id)
  97. end
  98. 1 def self.set_volume(membership:, params:, actor:)
  99. 3 actor.ability.authorize! :update, membership
  100. 3 val = Membership.volumes[params[:volume]]
  101. 3 if params[:apply_to_all]
  102. 1 group_ids = membership.group.parent_or_self.id_and_subgroup_ids
  103. 1 actor.memberships.where(group_id: group_ids).update_all(volume: val)
  104. 1 actor.discussion_readers.joins(:discussion).
  105. where('discussions.group_id': group_ids).
  106. update_all(volume: val)
  107. 1 Stance.joins(:poll).
  108. where('polls.group_id': group_ids).
  109. where(participant_id: actor.id).
  110. update_all(volume: val)
  111. else
  112. 2 membership.set_volume! params[:volume]
  113. 2 membership.discussion_readers.update_all(volume: val)
  114. 2 membership.stances.update_all(volume: val)
  115. end
  116. end
  117. 1 def self.resend(membership:, actor:)
  118. 3 actor.ability.authorize! :resend, membership
  119. 1 EventBus.broadcast 'membership_resend', membership, actor
  120. 1 Events::MembershipResent.publish!(membership, actor)
  121. end
  122. 1 def self.make_admin(membership:, actor:)
  123. actor.ability.authorize! :make_admin, membership
  124. membership.update admin: true
  125. Events::NewCoordinator.publish!(membership, actor)
  126. end
  127. 1 def self.remove_admin(membership:, actor:)
  128. actor.ability.authorize! :remove_admin, membership
  129. membership.update admin: false
  130. end
  131. 1 def self.join_group(group:, actor:)
  132. actor.ability.authorize! :join, group
  133. membership = group.add_member!(actor)
  134. EventBus.broadcast('membership_join_group', group, actor)
  135. Events::UserJoinedGroup.publish!(membership)
  136. end
  137. 1 def self.add_users_to_group(users:, group:, inviter:)
  138. inviter.ability.authorize!(:add_members, group)
  139. group.add_members!(users, inviter: inviter).tap do |memberships|
  140. Events::UserAddedToGroup.bulk_publish!(memberships, user: inviter)
  141. end
  142. end
  143. 1 def self.save_experience(membership:, actor:, params:)
  144. 2 actor.ability.authorize! :update, membership
  145. 1 membership.experienced!(params[:experience])
  146. 1 EventBus.broadcast('membership_save_experience', membership, actor, params)
  147. end
  148. end

app/services/merge_users_service.rb

72.22% lines covered

18 relevant lines. 13 lines covered and 5 lines missed.
    
  1. 1 class MergeUsersService
  2. 1 def self.send_merge_verification_email(actor:, target_email:)
  3. actor.ability.authorize! :update, actor
  4. target_user = User.active.find_by!(email: target_email)
  5. prep_for_merge!(source_user: actor, target_user: target_user)
  6. hash = MergeUsersService.build_merge_hash(source_user: actor, target_user: target_user)
  7. UserMailer.merge_verification(source_user: actor, target_user: target_user, hash: hash).deliver_now
  8. end
  9. 1 def self.prep_for_merge!(source_user:, target_user:)
  10. 2 source_user.update_attribute(:reset_password_token, User.generate_unique_secure_token)
  11. 2 target_user.update_attribute(:reset_password_token, User.generate_unique_secure_token)
  12. end
  13. 1 def self.validate(source_user:, target_user:, hash:)
  14. 2 return false if source_user.id == target_user.id
  15. 2 hash == build_merge_hash(source_user: source_user, target_user: target_user)
  16. end
  17. 1 def self.build_merge_hash(source_user:, target_user:)
  18. 3 sha1 = Digest::SHA1.new
  19. 3 sha1 << source_user.reset_password_token
  20. 3 sha1 << target_user.reset_password_token
  21. 3 sha1.hexdigest
  22. end
  23. end

app/services/message_channel_service.rb

91.3% lines covered

23 relevant lines. 21 lines covered and 2 lines missed.
    
  1. 1 class MessageChannelService
  2. 1 def self.publish_models(models, serializer: nil, scope: {}, root: nil, group_id: nil, user_id: nil)
  3. 2069 cache = RecordCache.for_collection(models, user_id)
  4. 2069 data = serialize_models(models, serializer: serializer, scope: scope.merge(cache: cache, current_user_id: user_id), root: root)
  5. 2069 publish_serialized_records(data, group_id: group_id, user_id: user_id)
  6. end
  7. 1 def self.serialize_models(models, serializer: nil, scope: {}, root: nil)
  8. 2070 models = Array(models)
  9. 2070 return unless model = models.first
  10. 1816 serializer ||= model.is_a?(Event) ? EventSerializer : "#{model.class}Serializer".constantize
  11. 1816 root ||= model.is_a?(Event) ? 'events' : model.class.to_s.pluralize.downcase
  12. 1816 ActiveModel::ArraySerializer.new(models, scope: scope, each_serializer: serializer, root: root)
  13. end
  14. 1 def self.publish_serialized_records(data, group_id: nil, user_id: nil)
  15. 2069 CACHE_REDIS_POOL.with do |client|
  16. 2069 room = "user-#{user_id}" if user_id
  17. 2069 room = "group-#{group_id}" if group_id
  18. 2069 data_str = data.as_json.as_json
  19. 2069 score = client.incr("/records/#{room}/score")
  20. # puts "incrementing score:", room, score, data_str
  21. 2069 client.zadd("/records/#{room}", score, data_str.to_json)
  22. 2069 client.publish("/records", {room: room, records: data_str, score: score}.to_json)
  23. 2069 client.zremrangebyscore("/records/#{room}", "-inf", (score - 200))
  24. end
  25. end
  26. 1 def self.publish_system_notice(notice, reload = false)
  27. CACHE_REDIS_POOL.with do |client|
  28. client.publish("/system_notice", {version: Loomio::Version.current,
  29. notice: notice,
  30. reload: reload}.to_json)
  31. end
  32. end
  33. end

app/services/migrate_events_service.rb

0.0% lines covered

68 relevant lines. 0 lines covered and 68 lines missed.
    
  1. module MigrateEventsService
  2. def self.migrate_edited_eventable
  3. Event.where(kind: ['poll_edited', 'discussion_edited'] ,
  4. eventable_type: "PaperTrail::Version").find_each do |event|
  5. version = event.eventable
  6. event.update_columns(eventable_type: version.item_type,
  7. eventable_id: version.item_id,
  8. custom_fields: {version_id: version.id,
  9. changed_keys: Hash(version.object_changes).keys})
  10. end
  11. Event.joins("LEFT OUTER JOIN polls on events.eventable_id = polls.id").where(eventable_type: "Poll").where("polls.id is null").destroy_all
  12. end
  13. def self.migrate_paperclip
  14. models = [Group, User, Discussion, Comment, Poll, Stance, Outcome, Document]
  15. models.each do |model|
  16. attachments = model.column_names.map do |c|
  17. if c =~ /(.+)_file_name$/
  18. $1
  19. end
  20. end.compact
  21. next if attachments.blank?
  22. attachments.each do |attachment|
  23. model.where("#{attachment}_file_name is not null").send("with_attached_#{attachment}").find_each.each do |instance|
  24. if instance.send(attachment).attachment.nil?
  25. puts "#{model.to_s} #{instance.id} " + instance.send("#{attachment}_file_name")
  26. MigrateAttachmentWorker.perform_async(model.to_s, instance.id, attachment)
  27. end
  28. end
  29. end
  30. end
  31. User.where("uploaded_avatar_file_name is not null").update_all(avatar_kind: "uploaded")
  32. end
  33. def self.rewrite_inline_images(host = nil)
  34. ActiveStorage::Attachment.where(name: 'image_files').includes(:record).order('id desc').each do |attachment|
  35. record = attachment.record
  36. column_name = names[attachment.record_type]
  37. next unless record[column_name].present?
  38. host ||= Regexp.escape ENV['CANONICAL_HOST']
  39. regex = /https:\/\/#{host}\/rails\/active_storage\/representations\/.*#{Regexp.escape URI.escape(attachment.filename.to_s)}/
  40. if record[column_name].match?(regex)
  41. path = Rails.application.routes.url_helpers.rails_representation_path(
  42. attachment.representation(HasRichText::PREVIEW_OPTIONS),
  43. only_path: true
  44. )
  45. puts "updating #{attachment.record_type} #{attachment.record_id}"
  46. record.update_columns(column_name => record[column_name].gsub(regex, path))
  47. end
  48. end
  49. end
  50. def self.rewrite_attachment_links
  51. names.each_pair do |type, col|
  52. type.constantize.where('attachments != ?', '[]').with_attached_files.each do |record|
  53. record.update_columns(attachments: record.build_attachments)
  54. end
  55. end
  56. end
  57. def self.names
  58. names = {
  59. 'Discussion' => 'description',
  60. 'Comment' => 'body',
  61. 'Poll' => 'details',
  62. 'Outcome' => 'statement',
  63. 'Stance' => 'reason',
  64. 'User' => 'short_bio',
  65. 'Group' => 'description',
  66. }
  67. end
  68. end

app/services/migrate_guests_service.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class MigrateGuestsService
  2. def self.migrate!
  3. DiscussionReader.joins(:discussion).where('discussions.group_id': nil).update_all(guest: true)
  4. Stance.joins(:poll).where('polls.group_id': nil).update_all(guest: true)
  5. Group.order('discussions_count desc').pluck(:id).each do |id|
  6. MigrateGuestOnDiscussionReadersAndStances.perform_async(id)
  7. end
  8. end
  9. end

app/services/mybb_service.rb

0.0% lines covered

64 relevant lines. 0 lines covered and 64 lines missed.
    
  1. class MybbService
  2. def self.convert_text(text)
  3. text.gsub('[hr]', '<hr>').gsub(/\[quote="[^\]]+\]/, '<blockquote>').gsub('[/quote]', '</blockquote>')
  4. end
  5. # note, I edited the posts.json and deleted the first couple of lines and the last couple too.
  6. # so I could read the file line by line and just consider each line a post
  7. # thats why I chop the trailing commas
  8. def self.import(filename, group_id)
  9. user_ids = {}
  10. discussion_ids = {}
  11. comment_ids = {}
  12. posts = URI.open(filename, 'r').map do |line|
  13. line.chop! if line.ends_with?("\n")
  14. line.chop! if line.ends_with?(',')
  15. JSON.parse(line)
  16. rescue
  17. byebug
  18. end
  19. ActiveRecord::Base.transaction do
  20. # create discussions
  21. posts.sort_by {|post| post['replyto'].to_i }.each do |post|
  22. # create user if not exists
  23. unless user_ids[post['uid']]
  24. email = "#{post['username'].parameterize.gsub('-','')}@mybb.example.com"
  25. u = User.find_by(email: email) || u = User.create!(
  26. name: post['username'],
  27. email: email
  28. )
  29. user_ids[post['uid']] = u.id
  30. end
  31. # create discussion if not exists
  32. if !discussion_ids[post['tid']]
  33. d = Discussion.create!(
  34. group_id: group_id,
  35. author_id: user_ids[post['uid']],
  36. title: post['subject'],
  37. description: convert_text(post['message']),
  38. created_at: Time.at(post['dateline'].to_i),
  39. updated_at: Time.at(post['dateline'].to_i)
  40. )
  41. d.create_missing_created_event!
  42. discussion_ids[post['tid']] = d.id
  43. else
  44. if post['message'].present?
  45. parent = Comment.find_by(id: post['replyto'].to_i)
  46. comment = Comment.create!(
  47. parent_id: comment_ids[parent&.id],
  48. discussion_id: discussion_ids[post['tid']],
  49. user_id: user_ids[post['uid']],
  50. created_at: Time.at(post['dateline'].to_i),
  51. updated_at: Time.at(post['dateline'].to_i),
  52. body: convert_text(post['message'])
  53. )
  54. comment_ids[post['pid']] = comment.id
  55. Event.create!(
  56. user_id: user_ids[post['uid']],
  57. discussion_id: discussion_ids[post['tid']],
  58. kind: "new_comment",
  59. eventable: comment,
  60. created_at: Time.at(post['dateline'].to_i),
  61. updated_at: Time.at(post['dateline'].to_i)
  62. )
  63. end
  64. end
  65. end
  66. end
  67. ids = Discussion.where(group_id: group_id).pluck(:id)
  68. ids.each {|id| EventService.repair_thread(id)}
  69. # SearchIndexWorker.new.perform(ids)
  70. end
  71. end

app/services/newsletter_service.rb

54.55% lines covered

22 relevant lines. 12 lines covered and 10 lines missed.
    
  1. 1 class NewsletterService
  2. 1 LISTMONK_URL = ENV.fetch('LISTMONK_URL', '')
  3. 1 LISTMONK_USERNAME = ENV.fetch('LISTMONK_USERNAME', nil)
  4. 1 LISTMONK_PASSWORD = ENV.fetch('LISTMONK_PASSWORD', nil)
  5. 1 LISTMONK_LIST_ID = ENV.fetch('LISTMONK_LIST_ID', 3)
  6. 1 def self.enabled?
  7. 6 LISTMONK_URL.starts_with?('http') && LISTMONK_USERNAME && LISTMONK_PASSWORD && LISTMONK_LIST_ID
  8. end
  9. 1 def self.subscribe(name, email)
  10. return unless enabled?
  11. HTTParty.post(
  12. "#{LISTMONK_URL}/api/subscribers",
  13. {
  14. basic_auth: auth,
  15. headers: { 'Content-Type' => 'application/json' },
  16. body: {
  17. email: parse_email(email),
  18. name: name,
  19. status: 'enabled',
  20. lists: [LISTMONK_LIST_ID.to_i],
  21. preconfirm_subscriptions: true,
  22. }.to_json,
  23. # :debug_output => $stdout
  24. }
  25. )
  26. end
  27. 1 def self.unsubscribe(email)
  28. 6 return unless enabled?
  29. response = HTTParty.get(
  30. "#{LISTMONK_URL}/api/subscribers",
  31. basic_auth: auth,
  32. query: {
  33. query: "subscribers.email LIKE '#{parse_email(email)}'"
  34. }
  35. )
  36. subscriber_id = response.dig('data', 'results', 0, 'id')
  37. return unless subscriber_id.present?
  38. HTTParty.delete(
  39. "#{LISTMONK_URL}/api/subscribers/#{subscriber_id}",
  40. basic_auth: auth
  41. )
  42. end
  43. 1 def self.auth
  44. {username: LISTMONK_USERNAME, password: LISTMONK_PASSWORD}
  45. end
  46. 1 def self.parse_email(email)
  47. ret = email.to_s.scan(AppConfig::EMAIL_REGEX).uniq.first
  48. raise "invalid email #{email}" unless ret.present?
  49. ret
  50. end
  51. end

app/services/notification_service.rb

88.0% lines covered

25 relevant lines. 22 lines covered and 3 lines missed.
    
  1. 1 class NotificationService
  2. 1 def self.mark_as_read(eventable_type, eventable_id, actor_id)
  3. 251 ids = Notification.joins(:event)
  4. .where(user_id: actor_id, viewed: false)
  5. .where('events.eventable_type': eventable_type, 'events.eventable_id': eventable_id).pluck(:id)
  6. 251 notifications = Notification.where(user_id: actor_id, id: ids, 'viewed': false)
  7. 251 notifications.update_all(viewed: true)
  8. 251 notifications.reload
  9. 251 MessageChannelService.publish_models(notifications, user_id: actor_id)
  10. end
  11. 1 def self.viewed_events(actor_id:, discussion_id: , sequence_ids: )
  12. 3 event_ids = []
  13. 3 events = Event.includes(:eventable).where(discussion_id: discussion_id, sequence_id: sequence_ids)
  14. 3 reactions = Reaction.where(reactable: events.map(&:eventable))
  15. 3 event_ids.concat Event.where(eventable: reactions).pluck(:id)
  16. 3 eventable_ids = {}
  17. 3 %w[Comment Discussion Poll Stance Outcome].each do |type|
  18. 15 eventable_ids[type] = Event.where(
  19. discussion_id: discussion_id,
  20. sequence_id: sequence_ids,
  21. eventable_type: type).pluck(:eventable_id)
  22. end
  23. 3 eventable_ids.each_pair do |type, ids|
  24. 15 event_ids.concat Notification.joins(:event).where(
  25. user_id: actor_id,
  26. viewed: false,
  27. 'events.eventable_type': type,
  28. 'events.eventable_id': ids).pluck('events.id')
  29. end
  30. 3 notifications = Notification.where(user_id: actor_id).
  31. where(event_id: event_ids.uniq).
  32. where('viewed': false)
  33. 3 notifications.update_all(viewed: true)
  34. 3 notifications.reload
  35. 3 MessageChannelService.publish_models(notifications, user_id: actor_id)
  36. end
  37. 1 def self.viewed(user:)
  38. user.notifications.where(viewed: false).update_all(viewed: true)
  39. notifications = user.notifications.includes(:actor, :user).order(created_at: :desc).limit(30)
  40. # alert clients (say, user's other tabs) that notifications have been read
  41. MessageChannelService.publish_models(notifications, user_id: user.id)
  42. end
  43. end

app/services/outcome_service.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. 1 class OutcomeService
  2. 1 def self.invite(outcome:, actor:, params:)
  3. 17 actor.ability.authorize! :announce, outcome
  4. 16 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  5. emails: params[:recipient_emails],
  6. audience: params[:recipient_audience],
  7. model: outcome,
  8. actor: actor)
  9. 15 users = UserInviter.where_or_create!(actor: actor,
  10. model: outcome,
  11. emails: params[:recipient_emails],
  12. user_ids: params[:recipient_user_ids],
  13. audience: params[:recipient_audience],
  14. include_actor: params[:include_actor].present?)
  15. 15 Events::OutcomeAnnounced.publish!(outcome, actor, users.pluck(:id), params[:recipient_audience])
  16. 15 users
  17. end
  18. 1 def self.create(outcome:, actor:, params: {})
  19. 29 actor.ability.authorize! :create, outcome
  20. 24 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  21. emails: params[:recipient_emails],
  22. audience: params[:recipient_audience],
  23. model: outcome,
  24. actor: actor)
  25. 24 outcome.assign_attributes(author: actor)
  26. 24 return false unless outcome.valid?
  27. 21 outcome.poll.outcomes.update_all(latest: false)
  28. 21 outcome.save!
  29. 21 users = UserInviter.where_or_create!(actor: actor,
  30. emails: params[:recipient_emails],
  31. user_ids: params[:recipient_user_ids],
  32. model: outcome,
  33. audience: params[:recipient_audience],
  34. include_actor: params[:include_actor].present?)
  35. 21 EventBus.broadcast 'outcome_create', outcome, actor
  36. 21 Events::OutcomeCreated.publish!(outcome: outcome,
  37. recipient_user_ids: users.pluck(:id),
  38. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  39. recipient_audience: params[:recipient_audience])
  40. end
  41. 1 def self.update(outcome:, actor:, params: {})
  42. 5 actor.ability.authorize! :update, outcome
  43. 2 UserInviter.authorize!(user_ids: params[:recipient_user_ids],
  44. emails: params[:recipient_emails],
  45. audience: params[:recipient_audience],
  46. model: outcome,
  47. actor: actor)
  48. 2 outcome.assign_attributes_and_files(params.slice(:review_on, :statement, :statement_format, :event_summary, :event_location, :files, :image_files, :link_previews, :poll_option_id))
  49. 2 return false unless outcome.valid?
  50. 1 outcome.save!
  51. 1 outcome.update_versions_count
  52. 1 users = UserInviter.where_or_create!(actor: actor,
  53. emails: params[:recipient_emails],
  54. user_ids: params[:recipient_user_ids],
  55. model: outcome,
  56. audience: params[:recipient_audience],
  57. include_actor: params[:include_actor].present?)
  58. 1 EventBus.broadcast 'outcome_update', outcome, actor
  59. 1 Events::OutcomeUpdated.publish!(outcome: outcome,
  60. actor: actor,
  61. recipient_user_ids: users.pluck(:id),
  62. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  63. recipient_audience: params[:recipient_audience])
  64. end
  65. 1 def self.publish_review_due
  66. 3 Outcome.review_due_not_published(Date.today).each do |outcome|
  67. 1 Events::OutcomeReviewDue.publish!(outcome)
  68. end
  69. end
  70. end

app/services/poll_service.rb

97.16% lines covered

141 relevant lines. 137 lines covered and 4 lines missed.
    
  1. 1 class PollService
  2. 1 def self.create(poll:, actor:, params: {})
  3. 172 actor.ability.authorize! :create, poll
  4. 167 poll.assign_attributes(author: actor)
  5. 167 poll.prioritise_poll_options!
  6. 167 return false unless poll.valid?
  7. 165 poll.save!
  8. 165 poll.update_counts!
  9. 165 if !poll.specified_voters_only
  10. 165 stances = create_stances(poll: poll, actor: actor, include_actor: true, audience: 'group')
  11. else
  12. stances = Stance.none
  13. end
  14. 165 user_ids = params[:notify_recipients] ? stances.pluck(:participant_id) : []
  15. 165 EventBus.broadcast('poll_create', poll, actor)
  16. 165 Events::PollCreated.publish!(poll, actor, recipient_user_ids: user_ids - [actor.id])
  17. end
  18. 1 def self.update(poll:, params:, actor:)
  19. 10 actor.ability.authorize! :update, poll
  20. 8 UserInviter.authorize!(
  21. user_ids: params[:recipient_user_ids],
  22. emails: params[:recipient_emails],
  23. audience: params[:recipient_audience],
  24. model: poll,
  25. actor: actor
  26. )
  27. 8 poll.assign_attributes_and_files(params.except(:poll_type, :discussion_id, :poll_template_id, :poll_template_key))
  28. # check again, because the group id could be updated to a untrusted group
  29. 8 actor.ability.authorize! :update, poll
  30. 7 poll.prioritise_poll_options!
  31. 7 return false unless poll.valid?
  32. 6 poll.save!
  33. 6 poll.update_counts!
  34. 6 GenericWorker.perform_async('SearchService', 'reindex_by_poll_id', poll.id)
  35. 6 users = UserInviter.where_or_create!(
  36. actor: actor,
  37. user_ids: params[:recipient_user_ids],
  38. emails: params[:recipient_emails],
  39. audience: params[:recipient_audience],
  40. model: poll
  41. )
  42. 6 EventBus.broadcast('poll_update', poll, actor)
  43. 6 Events::PollEdited.publish!(
  44. poll: poll,
  45. actor: actor,
  46. recipient_user_ids: users.pluck(:id),
  47. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  48. recipient_audience: params[:recipient_audience],
  49. recipient_message: params[:recipient_message]
  50. )
  51. end
  52. 1 def self.invite(poll:, actor:, params:)
  53. 50 UserInviter.authorize!(
  54. user_ids: params[:recipient_user_ids],
  55. emails: params[:recipient_emails],
  56. audience: params[:recipient_audience],
  57. model: poll,
  58. actor: actor,
  59. )
  60. 48 if poll.discussion
  61. 28 DiscussionService.add_users(
  62. discussion: poll.discussion,
  63. actor: actor,
  64. user_ids: params[:recipient_user_ids],
  65. emails: params[:recipient_emails],
  66. audience: params[:recipient_audience],
  67. )
  68. end
  69. 48 stances = create_stances(
  70. poll: poll, actor: actor,
  71. user_ids: params[:recipient_user_ids],
  72. emails: params[:recipient_emails],
  73. include_actor: params[:include_actor],
  74. audience: params[:recipient_audience]
  75. )
  76. 48 if params[:notify_recipients]
  77. 2 Events::PollAnnounced.publish!(
  78. poll: poll,
  79. actor: actor,
  80. stances: stances,
  81. recipient_user_ids: params[:recipient_user_ids],
  82. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  83. recipient_audience: params[:recipient_audience],
  84. recipient_message: params[:recipient_message],
  85. )
  86. end
  87. 48 stances
  88. end
  89. 1 def self.remind(poll:, actor:, params:)
  90. actor.ability.authorize! :remind, poll
  91. users = UserInviter.where_existing(
  92. user_ids: params[:recipient_user_ids],
  93. audience: params[:recipient_audience],
  94. model: poll,
  95. actor: actor
  96. )
  97. Events::PollReminder.publish!(
  98. poll: poll,
  99. actor: actor,
  100. recipient_user_ids: users.pluck(:id),
  101. recipient_chatbot_ids: params[:recipient_chatbot_ids],
  102. recipient_audience: params[:recipient_audience],
  103. recipient_message: params[:recipient_message]
  104. )
  105. end
  106. 1 def self.create_stances(poll:, actor:, user_ids: [], emails: [], audience: nil, include_actor: false)
  107. 748 existing_voter_ids = Stance.latest.where(poll_id: poll.id).pluck(:participant_id)
  108. 748 users = UserInviter.where_or_create!(
  109. actor: actor,
  110. model: poll,
  111. user_ids: user_ids,
  112. audience: audience,
  113. include_actor: include_actor,
  114. emails: emails
  115. ).where.not(id: existing_voter_ids)
  116. 748 volumes = {}
  117. 748 group_member_ids = (poll.group || NullGroup.new).member_ids
  118. 748 if poll.discussion_id
  119. 676 DiscussionReader.active.where(
  120. discussion_id: poll.discussion_id,
  121. user_id: users.pluck(:id),
  122. ).find_each do |dr|
  123. 116 volumes[dr.user_id] = dr.volume
  124. end
  125. end
  126. 748 if poll.group_id
  127. 742 Membership.active.where(
  128. group_id: poll.group_id,
  129. user_id: users.pluck(:id),
  130. ).find_each do |m|
  131. 1973 volumes[m.user_id] = m.volume unless volumes.has_key? m.user_id
  132. end
  133. end
  134. 748 reinvited_user_ids = Stance.revoked.where(poll_id: poll.id).pluck(:participant_id) & users.pluck(:id)
  135. 748 Stance.where(poll_id: poll.id, participant_id: reinvited_user_ids).each do |stance|
  136. 1 stance.update(revoked_at: nil, revoker_id: nil, inviter_id: actor.id, admin: false)
  137. end
  138. 748 new_stances = users.where.not(id: reinvited_user_ids).map do |user|
  139. 1980 Stance.new(
  140. participant: user,
  141. poll: poll,
  142. inviter: actor,
  143. guest: !group_member_ids.include?(user.id),
  144. volume: volumes[user.id] || user.default_membership_volume,
  145. latest: true,
  146. reason_format: user.default_format,
  147. created_at: Time.zone.now
  148. )
  149. end
  150. 748 Stance.import(new_stances, on_duplicate_key_ignore: true)
  151. 748 poll.reset_latest_stances!
  152. 748 poll.update_counts!
  153. 748 Stance.where(participant_id: users.pluck(:id), poll_id: poll.id, latest: true)
  154. end
  155. 1 def self.discard(poll:, actor:)
  156. 2 actor.ability.authorize!(:destroy, poll)
  157. 1 poll.update(discarded_at: Time.now, discarded_by: actor.id)
  158. 1 Event.where(kind: ["stance_created", "stance_updated"], eventable_id: poll.stances.pluck(:id)).update_all(discussion_id: nil)
  159. 1 poll.created_event.update!(user_id: nil, child_count: 0, pinned: false)
  160. 1 MessageChannelService.publish_models([poll.created_event], scope: {current_user: actor, current_user_id: actor.id}, group_id: poll.group_id)
  161. 1 poll.created_event
  162. end
  163. 1 def self.close(poll:, actor:)
  164. 9 actor.ability.authorize! :close, poll
  165. 6 do_closing_work(poll: poll)
  166. 6 Events::PollClosedByUser.publish!(poll, actor)
  167. end
  168. 1 def self.reopen(poll:, params:, actor:)
  169. 3 actor.ability.authorize! :reopen, poll
  170. 1 poll.assign_attributes(closing_at: params[:closing_at], closed_at: nil)
  171. 1 return false unless poll.valid?
  172. 1 poll.save!
  173. 1 EventBus.broadcast('poll_reopen', poll, actor)
  174. 1 Events::PollReopened.publish!(poll, actor)
  175. end
  176. 1 def self.publish_closing_soon
  177. 28 hour_start = 1.day.from_now.at_beginning_of_hour
  178. 28 hour_finish = hour_start + 1.hour
  179. 28 this_hour_tomorrow = hour_start..hour_finish
  180. 28 Poll.closing_soon_not_published(this_hour_tomorrow).each do |poll|
  181. 28 Events::PollClosingSoon.publish!(poll)
  182. end
  183. end
  184. 1 def self.group_members_added(group_id)
  185. 5869 member_ids = Group.find(group_id).members.humans.pluck(:id)
  186. 5869 Poll.active.where(group_id: group_id, specified_voters_only: false).each do |poll|
  187. 521 revoked_user_ids = poll.stances.revoked.pluck(:participant_id).uniq
  188. 521 PollService.create_stances(
  189. poll: poll,
  190. actor: poll.author,
  191. 521 user_ids: (member_ids - poll.voter_ids) - revoked_user_ids
  192. )
  193. 521 poll.update_counts!
  194. end
  195. end
  196. 1 def self.group_members_removed(group_id, removed_user_ids, actor_id, revoked_at)
  197. 10 Poll.active.where(group_id: group_id).each do |poll|
  198. 2 Stance.where(
  199. poll_id: poll.id,
  200. revoked_at: nil,
  201. participant_id: Array(removed_user_ids),
  202. ).update_all(revoked_at: revoked_at, revoker_id: actor_id)
  203. 2 poll.update_counts!
  204. end
  205. end
  206. 1 def self.expire_lapsed_polls
  207. 17 Poll.lapsed_but_not_closed.each do |poll|
  208. 15 CloseExpiredPollWorker.perform_async(poll.id)
  209. end
  210. end
  211. 1 def self.do_closing_work(poll:)
  212. 21 return if poll.closed_at
  213. 21 poll.stances.update_all(participant_id: nil) if poll.anonymous
  214. 21 if poll.discussion_id && poll.hide_results == 'until_closed'
  215. 1 stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
  216. 1 Event.where(kind: 'stance_created', eventable_id: stance_ids, discussion_id: nil).update_all(discussion_id: poll.discussion_id)
  217. 1 EventService.repair_thread(poll.discussion_id)
  218. end
  219. 21 poll.update_attribute(:closed_at, Time.now)
  220. 21 GenericWorker.perform_async('SearchService', 'reindex_by_poll_id', poll.id)
  221. end
  222. # def self.destroy(poll:, actor:)
  223. # actor.ability.authorize! :destroy, poll
  224. # poll.destroy
  225. #
  226. # EventBus.broadcast('poll_destroy', poll, actor)
  227. # end
  228. 1 def self.add_to_thread(poll:, params:, actor:)
  229. 1 discussion = Discussion.find(params[:discussion_id])
  230. 1 actor.ability.authorize! :update, poll
  231. 1 actor.ability.authorize! :update, discussion
  232. 1 ActiveRecord::Base.transaction do
  233. 1 poll.update(discussion_id: discussion.id, group_id: discussion.group.id)
  234. 1 event = poll.created_event
  235. 1 event.discussion_id = discussion.id
  236. 1 event.parent_id = discussion.created_event.id
  237. 1 event.pinned = true
  238. 1 event.set_sequences
  239. 1 event.save
  240. 1 poll.created_event.update_sequence_info!
  241. end
  242. 1 if (poll.closed? || poll.hide_results != 'until_closed')
  243. 1 stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
  244. 1 Event.where(kind: 'stance_created', eventable_id: stance_ids).update_all(discussion_id: poll.discussion_id)
  245. 1 EventService.repair_thread(poll.discussion_id)
  246. end
  247. 1 GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
  248. 1 poll.created_event
  249. end
  250. 1 def self.calculate_results(poll, poll_options)
  251. 648 sorted_poll_options = case poll.order_results_by
  252. when 'priority'
  253. 1016 poll_options.sort_by {|o| o.priority }
  254. else
  255. # when 'total_score_desc'
  256. 1502 poll_options.sort_by {|o| -(o.total_score)}
  257. end
  258. 648 l = sorted_poll_options.each_with_index.map do |option, index|
  259. 1870 option_name = poll.poll_option_name_format == 'i18n' ? "poll_#{poll.poll_type}_options."+option.name : option.name
  260. {
  261. 1870 id: option.id,
  262. poll_id: option.poll_id,
  263. name: option_name,
  264. name_format: poll.poll_option_name_format,
  265. icon: option.icon,
  266. rank: index+1,
  267. score: option.total_score,
  268. 1870 target_percent: ((option.icon == 'agree') && (poll.agree_target.to_i > 0)) ? ((option.total_score.to_f / poll.agree_target.to_f) * 100) : 0,
  269. 1870 score_percent: poll.total_score > 0 ? ((option.total_score.to_f / poll.total_score.to_f) * 100) : 0,
  270. 1870 max_score_percent: poll.total_score > 0 ? ((option.total_score.to_f / poll.stance_counts.max.to_f) * 100) : 0,
  271. 1870 voter_percent: poll.voters_count > 0 ? ((option.voter_count.to_f / poll.voters_count.to_f) * 100) : 0,
  272. average: option.average_score,
  273. voter_scores: option.voter_scores,
  274. voter_ids: option.voter_ids.take(500),
  275. voter_count: option.voter_count,
  276. color: option.color
  277. }.with_indifferent_access.freeze
  278. end
  279. 648 if poll.results_include_undecided
  280. 616 l.push({
  281. id: 0,
  282. poll_id: poll.id,
  283. name: 'poll_common_votes_panel.undecided',
  284. name_format: 'i18n',
  285. rank: nil,
  286. score: 0,
  287. score_percent: 0,
  288. max_score_percent: 0,
  289. 616 target_percent: poll.voters_count > 0 ? (poll.undecided_voters_count.to_f / poll.voters_count.to_f * 100) : 0,
  290. 616 voter_percent: poll.voters_count > 0 ? (poll.undecided_voters_count.to_f / poll.voters_count.to_f * 100) : 0,
  291. average: 0,
  292. voter_scores: {},
  293. voter_ids: poll.undecided_voters.map(&:id).take(500),
  294. voter_count: poll.undecided_voters_count,
  295. color: '#BBBBBB'
  296. }.with_indifferent_access.freeze)
  297. end
  298. 648 l
  299. end
  300. end

app/services/poll_template_service.rb

0.0% lines covered

62 relevant lines. 0 lines covered and 62 lines missed.
    
  1. class PollTemplateService
  2. def self.group_templates(group:)
  3. group.poll_templates.to_a.concat(
  4. default_templates.map do |template|
  5. template.position = group.poll_template_positions.fetch(template.key, 999)
  6. template.group_id = group.id
  7. template.discarded_at = DateTime.now if group.hidden_poll_templates.include?(template.key)
  8. template
  9. end
  10. )
  11. end
  12. def self.default_templates
  13. AppConfig.poll_templates.map do |key, raw_attrs|
  14. raw_attrs[:key] = key
  15. attrs = {}
  16. AppConfig.poll_types[raw_attrs['poll_type']]['defaults'].each_pair do |key, value|
  17. if key.match /_i18n$/
  18. attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
  19. else
  20. attrs[key] = value
  21. end
  22. end
  23. raw_attrs.each_pair do |key, value|
  24. if key.match /_i18n$/
  25. attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
  26. else
  27. attrs[key] = value
  28. end
  29. end
  30. attrs['poll_options'] = raw_attrs.fetch('poll_options', []).map do |raw_option|
  31. option = {}
  32. raw_option.each_pair do |key, value|
  33. if key.match /_i18n$/
  34. option[key.gsub(/_i18n$/, '')] = I18n.t(value)
  35. else
  36. option[key] = value
  37. end
  38. end
  39. option
  40. end
  41. PollTemplate.new attrs
  42. end
  43. end
  44. def self.create(poll_template:, actor:)
  45. actor.ability.authorize! :create, poll_template
  46. poll_template.assign_attributes(author: actor)
  47. return false unless poll_template.valid?
  48. if poll_template.key
  49. poll_template.group.hidden_poll_templates += Array(poll_template.key)
  50. poll_template.key = nil
  51. end
  52. poll_template.save!
  53. poll_template
  54. end
  55. def self.update(poll_template:, params:, actor:)
  56. actor.ability.authorize! :update, poll_template
  57. poll_template.assign_attributes_and_files(params.except(:group_id))
  58. return false unless poll_template.valid?
  59. poll_template.save!
  60. poll_template
  61. end
  62. end

app/services/reaction_service.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class ReactionService
  2. 1 def self.update(reaction:, params:, actor:)
  3. 5 actor.ability.authorize! :update, reaction
  4. 4 reaction.user = actor
  5. 4 reaction.assign_attributes(params.slice(:reaction))
  6. 4 return false unless reaction.valid?
  7. 4 reaction.save!
  8. 4 EventBus.broadcast 'reaction_create', reaction, actor
  9. 4 Events::ReactionCreated.publish!(reaction)
  10. end
  11. 1 def self.destroy(reaction:, actor:)
  12. 2 actor.ability.authorize! :destroy, reaction
  13. 1 reaction.destroy
  14. 1 EventBus.broadcast 'reaction_destroy', reaction, actor
  15. end
  16. end

app/services/received_email_service.rb

88.31% lines covered

77 relevant lines. 68 lines covered and 9 lines missed.
    
  1. 1 class ReceivedEmailService
  2. 1 def self.refresh_forward_email_rules
  3. forward_email_rules = File.readlines(Rails.root.join("db/default_forward_email_rules.txt")).map(&:chomp).map do |handle|
  4. {handle: handle, email: "#{handle}@#{ENV['REPLY_HOSTNAME']}"}
  5. end
  6. ForwardEmailRule.delete_all
  7. ForwardEmailRule.insert_all(forward_email_rules, record_timestamps: false)
  8. end
  9. 1 def self.route_all
  10. ReceivedEmail.unreleased.each do |email|
  11. route(email)
  12. end
  13. end
  14. 1 def self.route(email)
  15. 14 return nil unless email.route_address
  16. 14 return nil if email.released
  17. 14 return nil if email.sender_hostname.downcase == ENV['REPLY_HOSTNAME'].downcase
  18. 13 return nil if email.sender_hostname.downcase == ENV['SMTP_DOMAIN'].downcase
  19. 13 case email.route_path
  20. when /d=.+&u=.+&k=.+/
  21. # personal email-to-thread, eg. d=100&k=asdfghjkl&u=999@mail.loomio.com
  22. 3 if comment = CommentService.create(comment: Comment.new(comment_params(email)), actor: actor_from_email(email))
  23. 3 email.update_attribute(:released, true) if comment.persisted?
  24. end
  25. when /[^\s]+\+u=.+&k=.+/
  26. # personal email-to-group, eg. enspiral+u=99&k=adsfghjl@mail.loomio.com
  27. 1 if discussion = DiscussionService.create(discussion: Discussion.new(discussion_params(email)), actor: actor_from_email(email))
  28. 1 email.update_attribute(:released, true) if discussion.persisted?
  29. end
  30. else
  31. 9 if forward_email_rule = ForwardEmailRule.find_by(handle: email.route_path)
  32. 1 ForwardMailer.forward_message(
  33. from: "\"#{email.sender_name}\" <#{BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS}>",
  34. to: forward_email_rule.email,
  35. reply_to: email.from,
  36. subject: email.subject,
  37. body_text: email.body_text,
  38. body_html: email.body_html
  39. ).deliver_later
  40. 1 email.update(released: true)
  41. 1 return
  42. end
  43. 8 if group = Group.find_by(handle: email.route_path)
  44. 7 if !address_is_blocked(email, group)
  45. 6 email.update(group_id: group.id)
  46. 6 if actor = actor_from_email_and_group(email, group)
  47. 4 if discussion = DiscussionService.create(discussion: Discussion.new(discussion_params(email)), actor: actor)
  48. 3 email.update(released: true) if discussion.persisted?
  49. end
  50. else
  51. 2 Events::UnknownSender.publish!(email)
  52. end
  53. end
  54. end
  55. end
  56. rescue CanCan::AccessDenied, ActiveRecord::RecordNotFound
  57. # TODO handle when user is not allowed to comment or create discussion
  58. end
  59. 1 def self.extract_reply_body(text, author_name = nil)
  60. 11 return "" if text.strip.blank?
  61. 11 text.gsub!("\r\n", "\n")
  62. # some emails match multiple split points, we run this until there are none
  63. 239 while regex = reply_split_points(author_name).find { |regex| regex.match? text } do
  64. 8 text = text.split(regex).first.strip
  65. end
  66. 11 text.strip
  67. end
  68. 1 def self.delete_old_emails
  69. ReceivedEmail.where("created_at < ?", 60.days.ago).destroy_all
  70. end
  71. 1 private
  72. 1 def self.reply_split_points(author_name = nil)
  73. [
  74. 19 /^[[:space:]]*[-]+[[:space:]]*Original Message[[:space:]]*[-]+[[:space:]]*$/i,
  75. /^[[:space:]]*--[[:space:]]*$/,
  76. /^[[:space:]]*__[[:space:]]*$/,
  77. /^[[:space:]]*\>?[[:space:]]*On.*\n?.*wrote:\n?$/,
  78. /^[[:space:]]*\>?[[:space:]]*On.*\n?.*said:\n?$/,
  79. /^On.*<\r?\n?.*>.*\r?\n?wrote:\r?\n?$/,
  80. /On.*wrote:/,
  81. 19 (author_name ? /^[[:space:]]*#{author_name}[[:space:]]*$/ : nil), # signature that starts with author name
  82. /#{EventMailer::REPLY_DELIMITER}/,
  83. /\*?From:.*$/i,
  84. /^[[:space:]]*\d{4}[-\/]\d{1,2}[-\/]\d{1,2}[[:space:]].*[[:space:]]<.*>?$/i,
  85. /(_)*\n[[:space:]]*De :.*\n[[:space:]]*Envoyé :.*\n[[:space:]]*À :.*\n[[:space:]]*Objet :.*\n$/i, # French Outlook
  86. /^[[:space:]]*\>?[[:space:]]*Le.*<\n?.*>.*\n?a[[:space:]]?\n?écrit :$/, # French
  87. /^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribió:$/,
  88. /^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribiÃ:$/,
  89. /^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribió:$/
  90. ].compact
  91. end
  92. 1 def self.parse_route_params(route_path)
  93. 12 params = {}.with_indifferent_access
  94. 12 if route_path.include?('+')
  95. 2 params['handle'] = route_path.split('+').first
  96. end
  97. 12 route_path.split('+').last.split('&').each do |segment|
  98. 32 key_and_value = segment.split('=')
  99. 32 params[key_and_value[0]] = key_and_value[1]
  100. end
  101. 12 params
  102. end
  103. 1 def self.actor_from_email(email)
  104. 4 params = parse_route_params(email.route_path)
  105. 4 User.find_by!(id: params['u'], email_api_key: params['k'])
  106. end
  107. 1 def self.address_is_blocked(email, group)
  108. 7 MemberEmailAlias.blocked.find_by(email: email.sender_email, group_id: group.id)
  109. end
  110. 1 def self.actor_from_email_and_group(email, group)
  111. 6 if actor = (email.dkim_valid || email.spf_valid) && User.find_by(email: email.sender_email)
  112. 1 return actor if group.members.exists?(actor.id)
  113. end
  114. 5 if email_alias = MemberEmailAlias.allowed.find_by(email: email.sender_email, group_id: group.id)
  115. 4 return nil if email_alias.require_dkim && !email.dkim_valid
  116. 3 return nil if email_alias.require_spf && !email.spf_valid
  117. 3 return email_alias.user if group.members.exists?(email_alias.user.id)
  118. end
  119. nil
  120. end
  121. 1 def self.discussion_params(email)
  122. 5 params = parse_route_params(email.route_path)
  123. 5 group = Group.find_by!(handle: (params['handle'] || email.route_path))
  124. {
  125. 5 group_id: group.id,
  126. private: group.discussion_private_default,
  127. title: email.subject,
  128. body: email.full_body,
  129. body_format: email.body_format,
  130. files: email.attachments.map {|a| a.blob }
  131. }.compact
  132. end
  133. 1 def self.comment_params(email)
  134. 3 params = parse_route_params(email.route_path)
  135. 3 if params['c'].present?
  136. 1 parent_id = params['c']
  137. 1 parent_type = "Comment"
  138. end
  139. 3 if params['pt'].present?
  140. parent_type = {
  141. 1 'p' => 'Poll',
  142. 'c' => 'Comment',
  143. 's' => 'Stance',
  144. 'o' => 'Outcome'
  145. }[params['pt']]
  146. 1 parent_id = params['pi']
  147. end
  148. {
  149. 3 discussion_id: params['d'].to_i,
  150. parent_id: parent_id,
  151. parent_type: parent_type,
  152. body: email.reply_body,
  153. body_format: 'md',
  154. files: email.attachments.map {|a| a.blob }
  155. }.compact
  156. end
  157. end

app/services/record_cache.rb

96.53% lines covered

202 relevant lines. 195 lines covered and 7 lines missed.
    
  1. 1 class RecordCache
  2. 1 attr_accessor :scope
  3. 1 attr_accessor :exclude_types
  4. 1 attr_accessor :user_ids
  5. 1 attr_accessor :current_user_id
  6. 1 def initialize
  7. 2308 @scope = {}.with_indifferent_access
  8. 2308 @user_ids = []
  9. 2308 @exclude_types = []
  10. 2308 @current_user_id = nil
  11. end
  12. 1 def fetch(key_or_keys, id)
  13. 80713 (scope.dig(*Array(key_or_keys)) || {}).fetch(id) do
  14. 21936 if block_given?
  15. 21936 yield
  16. else
  17. raise "scope missing preloaded model: #{key_or_keys} #{id}"
  18. end
  19. end
  20. end
  21. 1 def self.for_collection(collection, user_id, exclude_types = [])
  22. 2308 obj = self.new
  23. 2308 obj.exclude_types = exclude_types
  24. 2308 obj.current_user_id = user_id
  25. 2308 return obj unless item = collection.to_a.first
  26. # puts "when #{item.class.to_s}"
  27. 2033 case item.class.to_s
  28. when 'Discussion'
  29. 50 collection_ids = collection.map(&:id)
  30. 50 obj.add_discussions(collection)
  31. 50 obj.add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id).compact))
  32. 50 obj.add_polls_options_stances_outcomes Poll.active.where(discussion_id: collection_ids)
  33. when 'Reaction'
  34. 1 obj.user_ids.concat collection.map(&:user_id)
  35. when 'Notification'
  36. 938 obj.add_events_complete Event.includes(:eventable).where(id: collection.map(&:event_id))
  37. # obj.add_events_eventables Event.includes(:eventable).where(id: collection.map(&:event_id))
  38. 938 obj.user_ids.concat collection.map(&:user_id)
  39. when 'Group'
  40. 11 obj.add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:id)))
  41. when 'Membership'
  42. 15 obj.add_groups Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id)))
  43. 15 obj.user_ids.concat collection.map(&:user_id).concat(collection.map(&:inviter_id).compact).compact.uniq
  44. when 'Poll'
  45. 13 obj.add_groups Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id)))
  46. 13 obj.add_discussions(Discussion.where(id: collection.map(&:discussion_id).uniq.compact))
  47. 13 obj.add_polls_options_stances_outcomes collection
  48. when 'Outcome'
  49. 3 obj.add_polls Poll.where(id: collection.map(&:poll_id))
  50. 3 obj.user_ids.concat collection.map(&:author_id)
  51. when 'Stance'
  52. 15 obj.add_stances(collection)
  53. 15 obj.add_polls_options_stances_outcomes Poll.kept.where(id: collection.map(&:poll_id))
  54. when 'User'
  55. # do nothing
  56. when 'DiscussionReader'
  57. 6 obj.user_ids.concat collection.map(&:user_id)
  58. when 'Comment'
  59. 2 obj.user_ids.concat collection.map(&:user_id)
  60. when 'MembershipRequest'
  61. 4 obj.user_ids.concat collection.map(&:requestor_id).concat(collection.map(&:responder_id)).compact.uniq
  62. when 'Document'
  63. 6 obj.user_ids.concat collection.map(&:author_id).compact
  64. when 'SearchResult'
  65. 3 obj.user_ids.concat collection.map(&:author_id).compact
  66. 3 obj.add_polls_options_stances_outcomes Poll.kept.where(id: collection.map(&:poll_id))
  67. else
  68. 938 obj.add_events_complete(collection) if item.is_a?(Event)
  69. end
  70. 2033 obj.add_users User.with_attached_uploaded_avatar.where(id: obj.user_ids)
  71. 2033 obj.add_discussion_readers(DiscussionReader.where(discussion_id: obj.discussion_ids, user_id: user_id))
  72. 2033 obj.add_events Event.where(kind: 'new_discussion', eventable_id: obj.discussion_ids)
  73. 2033 obj.add_events Event.where(kind: 'discussion_forked', eventable_id: obj.discussion_ids)
  74. 2033 obj.add_events Event.where(kind: 'poll_created', eventable_id: obj.poll_ids)
  75. 2033 obj.add_tags_complete
  76. 2033 obj
  77. end
  78. 1 def add_events_complete(collection)
  79. 1854 ids = {discussion: [], comment: [], group: [], poll: []}.with_indifferent_access
  80. 1854 Event.includes(:eventable).where(id: collection.map(&:id)).each do |e|
  81. 1865 ids[:discussion].push e.discussion_id if e.discussion_id
  82. 1865 next unless e.eventable
  83. 1865 ids[e.eventable_type.underscore] ||= []
  84. 1865 ids[e.eventable_type.underscore].push e.eventable_id
  85. 1865 if ['Stance', 'Outcome', 'PollOption'].include? e.eventable_type
  86. 160 ids[:poll].push e.eventable.poll_id
  87. end
  88. end
  89. 9458 ids.keys.each { |key| ids[key] = ids[key].uniq }
  90. # Eventable specific stuff
  91. # comments
  92. # ids[:comment].concat self.class.all_parent_ids_for(Comment, ids[:comment]) if ids[:comment].any?
  93. # find related group ids
  94. 1854 unless exclude_types.include?('group')
  95. 1845 ids[:group].concat Discussion.where(id: ids[:discussion]).pluck(:group_id)
  96. 1845 ids[:group].concat Poll.where(id: ids[:poll]).pluck(:group_id)
  97. # ids[:group].concat all_parent_ids_for(Group, ids[:group])
  98. end
  99. 1854 add_polls_options_stances_outcomes Poll.where(id: ids[:poll])
  100. 1854 add_discussions Discussion.where(id: ids[:discussion])
  101. 1854 add_events_eventables Event.includes(:eventable).where(id: self.class.ids_and_parent_ids(Event, collection.map(&:id)))
  102. 1854 add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids[:group])
  103. 1854 add_comments Comment.where(id: ids[:comment])
  104. # obj.add_reactions Reaction.where(id: ids[:reaction])
  105. # obj.add_group_subscriptions Group.includes(:subscription).where(id: ids[:group])
  106. # obj.add_events Event.where(kind: 'discussion_forked', eventable_id: @ids[:discussion])
  107. # obj.add_events Event.where(kind: 'poll_created', eventable_id: @ids[:poll])
  108. end
  109. 1 def self.ids_and_parent_ids(klass, ids)
  110. 1943 [ids, all_parent_ids_for(klass,ids)].flatten.uniq
  111. end
  112. 1 def self.all_parent_ids_for(klass, ids)
  113. 6869 return [] if ids.empty?
  114. 4926 parent_ids = klass.where(id: ids).pluck(:parent_id)
  115. 4926 [parent_ids, all_parent_ids_for(klass, parent_ids)].flatten.uniq
  116. end
  117. # remember to join subscriptions for this call
  118. 1 def add_groups_subscriptions_memberships(collection)
  119. 1915 return [] if exclude_types.include?('group')
  120. 1906 group_ids = add_groups(collection)
  121. 1906 add_memberships(Membership.active.where(group_id: group_ids, user_id: current_user_id), group_ids)
  122. 1906 add_subscriptions(collection)
  123. end
  124. 1 def add_groups(collection)
  125. 1934 return [] if exclude_types.include?('group')
  126. 1934 scope[:groups_by_id] ||= {}
  127. 1934 collection.map do |group|
  128. 1879 @user_ids.push group.creator_id
  129. 1879 scope[:groups_by_id][group.id] = group
  130. 1879 group.id
  131. end
  132. end
  133. # this is a colleciton of groups joined to subscription.. crazy I know
  134. 1 def add_subscriptions(collection)
  135. 1906 return [] if exclude_types.include?('subscription')
  136. 1906 scope[:subscriptions_by_group_id] ||= {}
  137. 1906 collection.each do |group|
  138. 1850 scope[:subscriptions_by_group_id][group.id] = group.subscription if group.subscription
  139. end
  140. end
  141. 1 def add_memberships(collection, group_ids)
  142. 1906 return if exclude_types.include?('membership')
  143. 1906 scope[:memberships_by_group_id] ||= {}
  144. 1906 scope[:memberships_by_id] ||= {}
  145. 1906 collection.each do |m|
  146. 993 @user_ids.push m.user_id
  147. 993 @user_ids.push m.inviter_id if m.inviter_id
  148. 993 scope[:memberships_by_group_id][m.group_id] = m
  149. 993 scope[:memberships_by_id][m.id] = m
  150. end
  151. # is this buggy?
  152. # our cache.fetch method benefits from knowing it is nil
  153. # group_ids.each do |id|
  154. # next if scope[:memberships_by_group_id].has_key?(id)
  155. # scope[:memberships_by_group_id][id] = nil
  156. # end
  157. end
  158. 1 def add_polls_options_stances_outcomes(collection)
  159. 1935 return if exclude_types.include?('poll')
  160. 1935 collection_ids = collection.map(&:id)
  161. 1935 add_polls collection
  162. 1935 add_poll_options PollOption.where(poll_id: collection_ids)
  163. 1935 add_stances Stance.latest.where(poll_id: collection_ids, participant_id: current_user_id)
  164. 1935 add_outcomes Outcome.latest.where(poll_id: collection_ids)
  165. end
  166. 1 def add_polls(collection)
  167. 1938 return if exclude_types.include?('poll')
  168. 1938 scope[:polls_by_discussion_id] ||= {}
  169. 1938 scope[:polls_by_id] ||= {}
  170. 1938 collection.each do |poll|
  171. 1224 @user_ids.push poll.author_id
  172. 1224 scope[:polls_by_id][poll.id] = poll
  173. 1224 scope[:polls_by_discussion_id][poll.discussion_id] ||= []
  174. 1224 scope[:polls_by_discussion_id][poll.discussion_id].push poll
  175. end
  176. end
  177. 1 def add_comments(collection)
  178. 1854 return [] if exclude_types.include?('comment')
  179. 1854 scope[:comments_by_id] ||= {}
  180. 1854 collection.each do |comment|
  181. 233 @user_ids.push comment.user_id
  182. 233 scope[:comments_by_id][comment.id] = comment
  183. end
  184. end
  185. 1 def add_tags_complete
  186. 2033 scope[:tags_by_type_and_id] ||= {}
  187. 2033 Tag.where(group_id: group_ids).each do |tag|
  188. 2127 scope[:tags_by_type_and_id]['Group'] ||= {}
  189. 2127 scope[:tags_by_type_and_id]['Group'][tag.group_id] ||= []
  190. 2127 scope[:tags_by_type_and_id]['Group'][tag.group_id].push tag
  191. end
  192. end
  193. 1 def add_outcomes(collection)
  194. 1935 return [] if exclude_types.include?('outcome')
  195. 1932 scope[:outcomes_by_id] ||= {}
  196. 1932 scope[:outcomes_by_poll_id] ||= {}
  197. 1932 collection.each do |outcome|
  198. 81 @user_ids.push outcome.author_id
  199. 81 scope[:outcomes_by_id][outcome.id] = outcome
  200. 81 scope[:outcomes_by_poll_id][outcome.poll_id] = outcome if outcome.latest
  201. end
  202. end
  203. 1 def add_reactions(collection)
  204. return [] if ids.empty?
  205. return [] if exclude_types.include?('reaction')
  206. scope[:reactions_by_id] ||= {}
  207. collection.each do |reaction|
  208. @user_ids.push reaction.user_id
  209. scope[:reactions_by_id][reaction.id] = reaction
  210. end
  211. end
  212. 1 def add_poll_options(collection)
  213. 1935 return [] if exclude_types.include?('poll_option')
  214. 1935 scope[:poll_options_by_id] ||= {}
  215. 1935 scope[:poll_options_by_poll_id] ||= {}
  216. 1935 collection.each do |poll_option|
  217. 4204 scope[:poll_options_by_id][poll_option.id] = poll_option
  218. 4204 scope[:poll_options_by_poll_id][poll_option.poll_id] ||= []
  219. 4204 scope[:poll_options_by_poll_id][poll_option.poll_id].push(poll_option)
  220. end
  221. end
  222. 1 def add_stances(collection)
  223. 1950 return [] if exclude_types.include?('stance')
  224. 1950 scope[:stances_by_id] ||= {}
  225. 1950 scope[:my_stances_by_poll_id] ||= {}
  226. 1950 collection.each do |stance|
  227. 915 @user_ids.push stance.participant_id
  228. 915 scope[:stances_by_id][stance.id] = stance
  229. 915 if stance.participant_id == current_user_id && stance.revoked_at.nil?
  230. 896 scope[:my_stances_by_poll_id][stance.poll_id] = stance
  231. end
  232. end
  233. end
  234. 1 def add_events(collection)
  235. 7953 return [] if exclude_types.include?('event')
  236. 7915 scope[:events_by_id] ||= {}
  237. 7915 scope[:events_by_kind_and_eventable_id] ||= {}
  238. 7915 collection.each do |event|
  239. 5627 @user_ids.push event.user_id if event.user_id
  240. 5627 scope[:events_by_id][event.id] = event
  241. 5627 scope[:events_by_kind_and_eventable_id][event.kind] ||= {}
  242. 5627 scope[:events_by_kind_and_eventable_id][event.kind][event.eventable_id] = event
  243. end
  244. end
  245. 1 def add_events_eventables(collection)
  246. 1854 events = collection.includes(:eventable)
  247. 1854 add_events(events)
  248. 1854 add_eventables(events.map(&:eventable).compact)
  249. end
  250. 1 def add_eventables(collection)
  251. 1854 collection.each do |eventable|
  252. 2897 @user_ids.push eventable.user_id if eventable.respond_to?(:user_id)
  253. 2897 scope["#{eventable.class.to_s.underscore.pluralize}_by_id"] ||= {}
  254. 2897 scope["#{eventable.class.to_s.underscore.pluralize}_by_id"][eventable.id] = eventable
  255. end
  256. end
  257. 1 def add_discussions(collection)
  258. 1917 return if exclude_types.include?('discussion')
  259. 1903 scope[:discussions_by_id] ||= {}
  260. 1903 collection.each do |discussion|
  261. 1461 @user_ids.push discussion.author_id
  262. 1461 scope[:discussions_by_id][discussion.id] = discussion
  263. end
  264. end
  265. 1 def add_discussion_readers(collection)
  266. 2033 return if exclude_types.include?('discussion_reader')
  267. 2033 scope[:discussion_readers_by_discussion_id] ||= {}
  268. 2033 collection.each do |dr|
  269. 95 scope[:discussion_readers_by_discussion_id][dr.discussion_id] = dr
  270. end
  271. end
  272. 1 def add_users(collection)
  273. 2033 return if exclude_types.include?('user')
  274. 2032 scope[:users_by_id] ||= {}
  275. 2032 collection.each do |user|
  276. 4134 scope[:users_by_id][user.id] = user
  277. end
  278. end
  279. 1 def group_ids
  280. 2033 scope.fetch(:groups_by_id, {}).keys
  281. end
  282. 1 def discussion_ids
  283. 6099 scope.fetch(:discussions_by_id, {}).keys
  284. end
  285. 1 def poll_ids
  286. 2033 scope.fetch(:polls_by_id, {}).keys
  287. end
  288. end

app/services/record_cloner.rb

72.87% lines covered

188 relevant lines. 137 lines covered and 51 lines missed.
    
  1. 1 class RecordCloner
  2. 1 def initialize(recorded_at:)
  3. 3 @recorded_at = recorded_at
  4. 3 @cache = {}
  5. end
  6. 1 def create_clone_group_for_public_demo(group, handle)
  7. clone_group = new_clone_group(group)
  8. clone_group.subscription = Subscription.new(plan: 'demo')
  9. clone_group.handle = handle
  10. clone_group.is_visible_to_public = true
  11. clone_group.members_can_create_subgroups = false
  12. clone_group.members_can_add_members = false
  13. clone_group.members_can_add_guests = false
  14. clone_group.members_can_announce = true
  15. clone_group.discussion_privacy_options = 'public_only'
  16. clone_group.membership_granted_upon = 'request'
  17. clone_group.discussions.each {|d| d.private = false }
  18. clone_group.polls.each {|p| p.specified_voters_only = false }
  19. clone_group.save!
  20. update_tag_colors(clone_group, group)
  21. clone_group.polls.each do |poll|
  22. poll.update_counts!
  23. poll.stances.each {|s| s.update_option_scores!}
  24. end
  25. clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
  26. clone_group.reload
  27. end
  28. 1 def update_tag_colors(clone_group, group)
  29. 1 group.tags.pluck(:name, :color).each do |pair|
  30. 2 Tag.where(group_id: clone_group.id, name: pair[0]).update_all(color: pair[1])
  31. end
  32. end
  33. 1 def create_clone_group_for_actor(group, actor)
  34. # we don't really use this one except for testing
  35. 1 clone_group = new_clone_group(group)
  36. 1 clone_group.creator = actor
  37. 1 clone_group.subscription = Subscription.new(plan: 'demo', owner: actor)
  38. 1 clone_group.save!
  39. 1 update_tag_colors(clone_group, group)
  40. 1 store_source_record_ids(clone_group)
  41. 1 clone_group.polls.each do |poll|
  42. 1 poll.update_counts!
  43. 2 poll.stances.each {|s| s.update_option_scores!}
  44. end
  45. 2 clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
  46. 1 clone_group.add_member! actor
  47. 1 clone_group.reload
  48. end
  49. 1 def clone_trial_content_into_group(group, actor)
  50. source_group = Group.find_by(handle: 'trial-group-template')
  51. group.discussions = source_group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
  52. group.polls = source_group.polls.kept.map {|p| new_clone_poll(p) }
  53. group.save!
  54. update_tag_colors(group, source_group)
  55. store_source_record_ids(group)
  56. TranslationService.translate_group_content!(group, actor.locale)
  57. group.polls.each do |poll|
  58. poll.update_counts!
  59. poll.stances.each {|s| s.update_option_scores!}
  60. end
  61. group.discussions.each {|d| EventService.repair_thread(d.id) }
  62. group.reload
  63. group.save!
  64. group
  65. end
  66. 1 def store_source_record_ids(clone_group)
  67. 1 source_ids = {}
  68. 1 @cache.each_pair do |key, value|
  69. 15 class_name, id = key.split('-')
  70. 15 source_ids["#{class_name}-#{value.id}"] = id.to_i
  71. end
  72. 1 clone_group.info['source_record_ids'] = source_ids
  73. 1 clone_group.save!
  74. end
  75. 1 def create_clone_group(group)
  76. clone_group = new_clone_group(group)
  77. clone_group.save!
  78. update_tag_colors(clone_group, group)
  79. store_source_record_ids(clone_group)
  80. clone_group.polls.each do |poll|
  81. poll.update_counts!
  82. poll.stances.each {|s| s.update_option_scores!}
  83. end
  84. clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
  85. clone_group.reload
  86. clone_group
  87. end
  88. 1 def new_clone_group(group, clone_parent = nil)
  89. copy_fields = %w[
  90. 1 name
  91. description
  92. description_format
  93. members_can_add_members
  94. members_can_edit_discussions
  95. members_can_edit_comments
  96. members_can_raise_motions
  97. members_can_vote
  98. members_can_start_discussions
  99. members_can_create_subgroups
  100. members_can_announce
  101. new_threads_max_depth
  102. new_threads_newest_first
  103. admins_can_edit_user_content
  104. members_can_add_guests
  105. members_can_delete_comments
  106. link_previews
  107. created_at
  108. updated_at
  109. category
  110. ]
  111. required_values = {
  112. 1 handle: nil,
  113. is_visible_to_public: false,
  114. is_visible_to_parent_members: false,
  115. discussion_privacy_options: 'private_only',
  116. membership_granted_upon: 'approval',
  117. listed_in_explore: false
  118. }
  119. 1 attachments = [:cover_photo, :logo, :files, :image_files]
  120. 1 clone_group = new_clone(group, copy_fields, required_values, attachments)
  121. 1 clone_group.parent = clone_parent
  122. 2 clone_group.memberships = group.memberships.map {|m| new_clone_membership(m) }
  123. 2 clone_group.discussions = group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
  124. 1 clone_group.subgroups = group.subgroups.published.map {|g| new_clone_group(g, clone_group) }
  125. 2 clone_group.polls = group.polls.kept.map {|p| new_clone_poll(p) }
  126. 1 clone_group
  127. end
  128. 1 def new_clone_discussion(discussion)
  129. copy_fields = %w[
  130. 2 author_id
  131. title
  132. discussion_template_id
  133. discussion_template_key
  134. description
  135. description_format
  136. pinned_at
  137. max_depth
  138. newest_first
  139. content_locale
  140. link_previews
  141. created_at
  142. updated_at
  143. closed_at
  144. last_activity_at
  145. discarded_at
  146. template
  147. tags
  148. ]
  149. 2 required_values = {
  150. private: true
  151. }
  152. 2 attachments = [:files, :image_files]
  153. 2 new_clone(discussion, copy_fields, required_values, attachments)
  154. end
  155. 1 def new_clone_discussion_and_events(discussion)
  156. 2 clone_discussion = new_clone_discussion(discussion)
  157. 2 created_event = new_clone_event(discussion.created_event)
  158. 2 created_event.eventable = clone_discussion
  159. 2 clone_discussion.events << created_event
  160. 2 drop_kinds = %w[poll_closed_by_user poll_expired poll_reopened]
  161. 18 clone_discussion.items = discussion.items.order(:sequence_id).select{|i| !drop_kinds.include?(i.kind) }.map { |event| new_clone_event_and_eventable(event) }
  162. 4 clone_discussion.polls = discussion.polls.map {|p| new_clone_poll(p) }
  163. 4 clone_discussion.comments = discussion.comments.order(:id).map { |c| new_clone_comment(c) }
  164. 2 clone_discussion
  165. end
  166. 1 def new_clone_poll(poll)
  167. copy_fields = %w[
  168. 6 author_id
  169. closing_at
  170. closed_at
  171. created_at
  172. updated_at
  173. discarded_at
  174. title
  175. details
  176. poll_type
  177. process_name
  178. process_subtitle
  179. voter_can_add_options
  180. anonymous
  181. details_format
  182. hide_results
  183. discarded_by
  184. specified_voters_only
  185. notify_on_closing_soon
  186. content_locale
  187. link_previews
  188. shuffle_options
  189. limit_reason_length
  190. meeting_duration
  191. time_zone
  192. dots_per_person
  193. minimum_stance_choices
  194. maximum_stance_choices
  195. can_respond_maybe
  196. min_score
  197. max_score
  198. template
  199. agree_target
  200. chart_type
  201. default_duration_in_days
  202. stance_reason_required
  203. poll_option_name_format
  204. reason_prompt
  205. tags
  206. poll_template_id
  207. poll_template_key
  208. ]
  209. 6 attachments = [:files, :image_files]
  210. 6 clone_poll = new_clone(poll, copy_fields, {}, attachments)
  211. 25 clone_poll.poll_options = poll.poll_options.map {|poll_option| new_clone_poll_option(poll_option) }
  212. 12 clone_poll.stances = poll.stances.map {|stance| new_clone_stance(stance) }
  213. 12 clone_poll.outcomes = poll.outcomes.map {|outcome| new_clone_outcome(outcome) }
  214. 6 if !clone_poll.template
  215. 6 if poll.outcomes.empty?
  216. clone_poll.closed_at = nil
  217. clone_poll.closing_at = 3.days.from_now
  218. else
  219. 6 clone_poll.closed_at = poll.outcomes.first.created_at
  220. end
  221. end
  222. 6 clone_poll
  223. end
  224. 1 def new_clone_poll_option(poll_option)
  225. copy_fields = %w[
  226. 19 name
  227. icon
  228. meaning
  229. prompt
  230. priority
  231. score_counts
  232. total_score
  233. voter_scores
  234. voter_count
  235. ]
  236. 19 clone_poll_option = new_clone(poll_option, copy_fields)
  237. 19 clone_poll_option.poll = existing_clone(poll_option.poll)
  238. 19 clone_poll_option
  239. end
  240. 1 def new_clone_stance(stance)
  241. copy_fields = %w[
  242. 8 accepted_at
  243. admin
  244. cast_at
  245. content_locale
  246. inviter_id
  247. latest
  248. link_previews
  249. participant_id
  250. reason
  251. reason_format
  252. revoked_at
  253. created_at
  254. updated_at
  255. volume
  256. ]
  257. 8 attachments = [:files, :image_files]
  258. 8 clone_stance = new_clone(stance, copy_fields, {}, attachments)
  259. 16 clone_stance.stance_choices = stance.stance_choices.map {|sc| new_clone_stance_choice(sc) }
  260. 8 clone_stance.poll = existing_clone(stance.poll)
  261. 8 clone_stance
  262. end
  263. 1 def new_clone_stance_choice(sc)
  264. 8 copy_fields = %w[ score ]
  265. 8 clone_sc = new_clone(sc, copy_fields)
  266. 8 clone_sc.poll_option = existing_clone(sc.poll_option)
  267. 8 clone_sc
  268. end
  269. 1 def new_clone_outcome(outcome)
  270. copy_fields = %w[
  271. 8 statement
  272. latest
  273. statement_format
  274. author_id
  275. review_on
  276. content_locale
  277. link_previews
  278. created_at
  279. updated_at
  280. ]
  281. 8 attachments = [:files, :image_files]
  282. 8 clone_outcome = new_clone(outcome, copy_fields, {}, attachments)
  283. end
  284. 1 def new_clone_event(event)
  285. copy_fields = %w[
  286. 10 user_id
  287. kind
  288. depth
  289. sequence_id
  290. position
  291. position_key
  292. child_count
  293. pinned
  294. descendant_count
  295. custom_fields
  296. created_at
  297. ]
  298. 10 new_clone(event, copy_fields)
  299. end
  300. 1 def new_clone_event_and_eventable(event)
  301. 8 clone_event = new_clone_event(event)
  302. 8 case event.eventable_type
  303. when 'Poll'
  304. 2 clone_event.eventable = new_clone_poll(event.eventable)
  305. when 'Comment'
  306. 2 clone_event.eventable = new_clone_comment(event.eventable)
  307. when 'Stance'
  308. 2 clone_event.eventable = new_clone_stance(event.eventable)
  309. when 'Outcome'
  310. 2 clone_event.eventable = new_clone_outcome(event.eventable)
  311. when 'Discussion'
  312. clone_event.eventable = new_clone_discussion(event.eventable)
  313. when nil
  314. # nothing
  315. else
  316. raise "unrecognised eventable_type #{event.eventable_type}"
  317. end
  318. 8 clone_event
  319. end
  320. 1 def new_clone_membership(membership)
  321. copy_fields = %w[
  322. 1 user_id
  323. inviter_id
  324. revoked_at
  325. revoker_id
  326. admin
  327. volume
  328. experiences
  329. accepted_at
  330. title
  331. ]
  332. 1 clone_membership = new_clone(membership, copy_fields)
  333. 1 clone_membership.group = existing_clone(membership.group)
  334. 1 clone_membership
  335. end
  336. 1 def new_clone_comment(comment)
  337. copy_fields = %w[
  338. 4 user_id
  339. body
  340. body_format
  341. discarded_at
  342. discarded_by
  343. content_locale
  344. link_previews
  345. created_at
  346. ]
  347. 4 attachments = [:files, :image_files]
  348. 4 clone_comment = new_clone(comment, copy_fields, {}, attachments)
  349. 4 clone_comment.discussion = existing_clone(comment.discussion)
  350. 4 clone_comment.parent = existing_clone(comment.parent)
  351. 4 clone_comment
  352. end
  353. 1 def new_clone_tag(tag)
  354. clone_tag = new_clone(tag, %w[name color priority])
  355. clone_tag.group = existing_clone(tag.group)
  356. clone_tag
  357. end
  358. 1 def new_clone(record, copy_fields = [], required_values = {}, attachments = [])
  359. 67 @cache["#{record.class}-#{record.id}"] ||= begin
  360. 39 clone = record.class.new
  361. 39 record_type = record.class.to_s.underscore.to_sym
  362. 39 clone.attributes = new_clone_attributes(record, copy_fields, required_values)
  363. 39 attachments.each do |name|
  364. 30 if clone.send(name).class == ActiveStorage::Attached::Many
  365. 28 clone.send(name).attach(record.send(name).blobs)
  366. else
  367. 2 clone.send(name).attach record.send(name).blob
  368. end
  369. end
  370. 39 clone
  371. end
  372. end
  373. 1 def new_clone_attributes(record, copy_fields = [], required_values = {})
  374. 39 attrs = {}
  375. 39 copy_fields.each do |field|
  376. 482 value = record.send(field)
  377. 482 if value.nil?
  378. 103 attrs[field] = value
  379. 379 elsif field.ends_with?('_at')
  380. 45 attrs[field] = value.to_datetime + (DateTime.now - @recorded_at.to_datetime)
  381. 334 elsif field.ends_with?('_on')
  382. attrs[field] = value.to_date + (Date.today - @recorded_at.to_date)
  383. else
  384. 334 attrs[field] = value
  385. end
  386. end
  387. 39 required_values.each_pair do |key, value|
  388. 8 attrs[key] = value
  389. end
  390. 39 attrs
  391. end
  392. 1 def existing_clone(record)
  393. 44 @cache["#{record.class}-#{record.id}"]
  394. end
  395. end

app/services/report_service.rb

0.0% lines covered

413 relevant lines. 0 lines covered and 413 lines missed.
    
  1. class ReportService
  2. def initialize(interval: 'month', group_ids: nil, start_at: 6.months.ago, end_at: 1.minute.ago)
  3. @interval = interval
  4. @group_ids = group_ids
  5. @start_at = start_at
  6. @end_at = end_at
  7. @direct_threads = @group_ids.include?(0)
  8. end
  9. def intervals
  10. vals = []
  11. case @interval
  12. when 'year'
  13. next_val = @start_at.to_date.at_beginning_of_year
  14. when 'month'
  15. next_val = @start_at.to_date.at_beginning_of_month
  16. when 'week'
  17. next_val = @start_at.to_date.at_beginning_of_week
  18. when 'day'
  19. next_val = @start_at.to_date
  20. else
  21. raise "invalid interval value: #{@interval}"
  22. end
  23. while next_val < @end_at
  24. vals.push(next_val)
  25. next_val = (next_val + 1.send(@interval)).to_date
  26. end
  27. vals
  28. end
  29. # need to remember to sanitize group ids, any other args
  30. def rows_to_hash(results, name_a = 'interval', name_b = 'count')
  31. results.to_a.map {|row| [row[name_a], row[name_b]]}.to_h
  32. end
  33. def discussions_per_interval
  34. query = <<~SQL
  35. SELECT date_trunc('#{@interval}', discussions.created_at)::date AS interval, count(discussions.id) count
  36. FROM discussions
  37. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  38. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  39. group by interval
  40. SQL
  41. rows_to_hash ActiveRecord::Base.connection.execute query
  42. end
  43. def comments_per_interval
  44. query = <<~SQL
  45. SELECT date_trunc('#{@interval}', comments.created_at)::date AS interval, count(comments.id) count
  46. FROM comments
  47. LEFT JOIN discussions ON comments.discussion_id = discussions.id
  48. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  49. AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  50. group by interval
  51. SQL
  52. rows_to_hash ActiveRecord::Base.connection.execute query
  53. end
  54. def polls_per_interval
  55. query = <<~SQL
  56. SELECT date_trunc('#{@interval}', polls.created_at)::date AS interval, count(polls.id) count
  57. FROM polls
  58. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  59. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  60. group by interval
  61. SQL
  62. rows_to_hash ActiveRecord::Base.connection.execute query
  63. end
  64. def stances_per_interval
  65. query = <<~SQL
  66. SELECT date_trunc('#{@interval}', stances.created_at)::date AS interval, count(stances.id) count
  67. FROM stances
  68. LEFT JOIN polls ON stances.poll_id = polls.id
  69. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  70. AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  71. AND stances.latest IS true
  72. AND stances.cast_at IS NOT NULL
  73. group by interval
  74. SQL
  75. rows_to_hash ActiveRecord::Base.connection.execute query
  76. end
  77. def outcomes_per_interval
  78. query = <<~SQL
  79. SELECT date_trunc('#{@interval}', outcomes.created_at)::date AS interval, count(outcomes.id) count
  80. FROM outcomes
  81. LEFT JOIN polls ON outcomes.poll_id = polls.id
  82. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  83. AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  84. group by interval
  85. SQL
  86. rows_to_hash ActiveRecord::Base.connection.execute query
  87. end
  88. def discussions_count
  89. query = <<~SQL
  90. SELECT count(discussions.id) count
  91. FROM discussions
  92. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  93. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  94. SQL
  95. ActiveRecord::Base.connection.execute(query).to_a.first['count']
  96. end
  97. def polls_count
  98. query = <<~SQL
  99. SELECT count(polls.id) count
  100. FROM polls
  101. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  102. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  103. SQL
  104. ActiveRecord::Base.connection.execute(query).to_a.first['count']
  105. end
  106. def discussions_with_polls_count
  107. query = <<~SQL
  108. SELECT count(discussions.id) count
  109. FROM discussions INNER JOIN polls ON discussions.id = polls.discussion_id
  110. WHERE (discussions.group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR discussions.group_id IS NULL' : ''})
  111. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  112. SQL
  113. ActiveRecord::Base.connection.execute(query).to_a.first['count']
  114. end
  115. def polls_with_outcomes_count
  116. query = <<~SQL
  117. SELECT count(polls.id) count
  118. FROM polls INNER JOIN outcomes ON polls.id = outcomes.poll_id
  119. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  120. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  121. SQL
  122. ActiveRecord::Base.connection.execute(query).to_a.first['count']
  123. end
  124. def discussion_ids
  125. query = <<~SQL
  126. SELECT discussions.id
  127. FROM discussions
  128. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  129. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  130. SQL
  131. ActiveRecord::Base.connection.execute(query).map { |row| row['id'] }
  132. end
  133. def poll_ids
  134. query = <<~SQL
  135. SELECT polls.id
  136. FROM polls
  137. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  138. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  139. SQL
  140. ActiveRecord::Base.connection.execute(query).map { |row| row['id'] }
  141. end
  142. def discussion_tag_counts
  143. tag_counts = {}
  144. Discussion.where(id: discussion_ids).each do |discussion|
  145. discussion.tags.each {|tag| tag_counts[tag] = tag_counts.fetch(tag, 0) + 1 }
  146. end
  147. tag_counts
  148. end
  149. def poll_tag_counts
  150. tag_counts = {}
  151. Poll.where(id: poll_ids).each do |poll|
  152. poll.tags.each {|tag| tag_counts[tag] = tag_counts.fetch(tag, 0) + 1 }
  153. end
  154. tag_counts
  155. end
  156. def tag_counts
  157. total_counts = {}
  158. discussion_tag_counts.each_pair {|tag, count| total_counts[tag] = total_counts.fetch(tag, 0) + count }
  159. poll_tag_counts.each_pair {|tag, count| total_counts[tag] = total_counts.fetch(tag, 0) + count }
  160. total_counts
  161. end
  162. def tag_names
  163. (discussion_tag_counts.keys + poll_tag_counts.keys).uniq.sort
  164. end
  165. def discussions_per_user
  166. query = <<~SQL
  167. SELECT count(id) count, author_id user_id
  168. FROM discussions
  169. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  170. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  171. group by author_id
  172. SQL
  173. rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
  174. end
  175. def comments_per_user
  176. query = <<~SQL
  177. SELECT count(comments.id) count, user_id
  178. FROM comments
  179. JOIN discussions ON comments.discussion_id = discussions.id
  180. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  181. AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  182. group by user_id
  183. SQL
  184. rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
  185. end
  186. def polls_per_user
  187. query = <<~SQL
  188. SELECT count(id) count, author_id user_id
  189. FROM polls
  190. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  191. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  192. group by author_id
  193. SQL
  194. rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
  195. end
  196. def outcomes_per_user
  197. query = <<~SQL
  198. SELECT count(outcomes.id) count, outcomes.author_id user_id
  199. FROM outcomes
  200. JOIN polls ON outcomes.poll_id = polls.id
  201. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  202. AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  203. group by outcomes.author_id
  204. SQL
  205. rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
  206. end
  207. def stances_per_user
  208. query = <<~SQL
  209. SELECT count(stances.id) count, participant_id
  210. FROM stances
  211. JOIN polls ON stances.poll_id = polls.id
  212. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  213. AND polls.anonymous = false
  214. AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  215. AND stances.latest IS true
  216. AND stances.cast_at IS NOT NULL
  217. group by participant_id
  218. SQL
  219. rows_to_hash ActiveRecord::Base.connection.execute(query), 'participant_id', 'count'
  220. end
  221. def reactions_per_user
  222. queries = []
  223. data = {}
  224. queries.push <<~SQL
  225. SELECT count(reactions.id) count, reactions.user_id user_id
  226. FROM reactions
  227. JOIN comments ON reactions.reactable_id = comments.id AND reactions.reactable_type = 'Comment'
  228. JOIN discussions ON comments.discussion_id = discussions.id
  229. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  230. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  231. group by reactions.user_id
  232. SQL
  233. queries.push <<~SQL
  234. SELECT count(reactions.id) count, reactions.user_id user_id
  235. FROM reactions
  236. JOIN discussions ON reactions.reactable_id = discussions.id AND reactions.reactable_type = 'Discussion'
  237. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  238. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  239. group by reactions.user_id
  240. SQL
  241. queries.push <<~SQL
  242. SELECT count(reactions.id) count, reactions.user_id user_id
  243. FROM reactions
  244. JOIN polls ON reactions.reactable_id = polls.id AND reactions.reactable_type = 'Poll'
  245. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  246. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  247. group by reactions.user_id
  248. SQL
  249. queries.push <<~SQL
  250. SELECT count(reactions.id) count, reactions.user_id user_id
  251. FROM reactions
  252. JOIN stances ON reactions.reactable_id = stances.id AND reactions.reactable_type = 'Stance'
  253. JOIN polls ON stances.poll_id = polls.id
  254. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  255. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  256. group by reactions.user_id
  257. SQL
  258. queries.push <<~SQL
  259. SELECT count(reactions.id) count, reactions.user_id user_id
  260. FROM reactions
  261. JOIN outcomes ON reactions.reactable_id = outcomes.id AND reactions.reactable_type = 'Outcome'
  262. JOIN polls ON outcomes.poll_id = polls.id
  263. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  264. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  265. group by reactions.user_id
  266. SQL
  267. queries.each do |query|
  268. rows_to_hash(ActiveRecord::Base.connection.execute(query), 'user_id', 'count').each_pair do |k, v|
  269. data[k] = data.fetch(k, 0) + v
  270. end
  271. end
  272. data
  273. end
  274. def users
  275. if @direct_threads
  276. User.all
  277. else
  278. user_ids = Membership.where(group_id: @group_ids).pluck(:user_id).uniq
  279. User.where(id: user_ids)
  280. end
  281. end
  282. def users_per_country
  283. query = <<~SQL
  284. SELECT count(DISTINCT users.id) count, country
  285. FROM users
  286. JOIN memberships ON memberships.user_id = users.id
  287. WHERE email_verified = true
  288. #{@direct_threads ? '' : "AND memberships.group_id IN (#{@group_ids.join(',')})"}
  289. group by users.country
  290. SQL
  291. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  292. end
  293. def discussions_per_country
  294. query = <<~SQL
  295. SELECT count(discussions.id) count, country
  296. FROM discussions
  297. JOIN users ON discussions.author_id = users.id
  298. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  299. AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  300. group by users.country
  301. SQL
  302. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  303. end
  304. def comments_per_country
  305. query = <<~SQL
  306. SELECT count(comments.id) count, country
  307. FROM comments
  308. JOIN discussions ON comments.discussion_id = discussions.id
  309. JOIN users ON comments.user_id = users.id
  310. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  311. AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  312. group by users.country
  313. SQL
  314. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  315. end
  316. def polls_per_country
  317. query = <<~SQL
  318. SELECT count(polls.id) count, country
  319. FROM polls
  320. JOIN users ON polls.author_id = users.id
  321. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  322. AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  323. group by users.country
  324. SQL
  325. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  326. end
  327. def outcomes_per_country
  328. query = <<~SQL
  329. SELECT count(outcomes.id) count, country
  330. FROM outcomes
  331. JOIN polls ON polls.id = outcomes.poll_id
  332. JOIN users ON outcomes.author_id = users.id
  333. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  334. AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  335. group by users.country
  336. SQL
  337. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  338. end
  339. def stances_per_country
  340. query = <<~SQL
  341. SELECT count(stances.id) count, country
  342. FROM stances
  343. JOIN polls ON stances.poll_id = polls.id
  344. JOIN users ON stances.participant_id = users.id
  345. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  346. AND polls.anonymous = false
  347. AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  348. AND stances.latest IS true
  349. AND stances.cast_at IS NOT NULL
  350. group by country
  351. SQL
  352. rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
  353. end
  354. def reactions_per_country
  355. queries = []
  356. data = {}
  357. queries.push <<~SQL
  358. SELECT count(reactions.id) count, country
  359. FROM reactions
  360. JOIN comments ON reactions.reactable_id = comments.id AND reactions.reactable_type = 'Comment'
  361. JOIN discussions ON comments.discussion_id = discussions.id
  362. JOIN users ON reactions.user_id = users.id
  363. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  364. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  365. group by country
  366. SQL
  367. queries.push <<~SQL
  368. SELECT count(reactions.id) count, country
  369. FROM reactions
  370. JOIN discussions ON reactions.reactable_id = discussions.id AND reactions.reactable_type = 'Discussion'
  371. JOIN users ON reactions.user_id = users.id
  372. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  373. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  374. group by country
  375. SQL
  376. queries.push <<~SQL
  377. SELECT count(reactions.id) count, country
  378. FROM reactions
  379. JOIN polls ON reactions.reactable_id = polls.id AND reactions.reactable_type = 'Poll'
  380. JOIN users ON reactions.user_id = users.id
  381. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  382. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  383. group by country
  384. SQL
  385. queries.push <<~SQL
  386. SELECT count(reactions.id) count, country
  387. FROM reactions
  388. JOIN stances ON reactions.reactable_id = stances.id AND reactions.reactable_type = 'Stance'
  389. JOIN polls ON stances.poll_id = polls.id
  390. JOIN users ON reactions.user_id = users.id
  391. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  392. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  393. group by country
  394. SQL
  395. queries.push <<~SQL
  396. SELECT count(reactions.id) count, country
  397. FROM reactions
  398. JOIN outcomes ON reactions.reactable_id = outcomes.id AND reactions.reactable_type = 'Outcome'
  399. JOIN polls ON outcomes.poll_id = polls.id
  400. JOIN users ON reactions.user_id = users.id
  401. WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
  402. AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
  403. group by country
  404. SQL
  405. queries.each do |query|
  406. rows_to_hash(ActiveRecord::Base.connection.execute(query), 'country', 'count').each_pair do |k, v|
  407. data[k] = data.fetch(k, 0) + v
  408. end
  409. end
  410. data
  411. end
  412. def countries
  413. users.pluck(:country).uniq.map {|c| c.nil? ? 'Unknown' : c }.sort
  414. end
  415. end

app/services/retry_on_error.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module RetryOnError
  2. 1 def self.with_limit(limit)
  3. 474 limit.times do |i|
  4. begin
  5. 478 return yield i
  6. rescue => e
  7. 5 raise e if i + 1 == limit
  8. end
  9. end
  10. end
  11. end

app/services/search_service.rb

78.95% lines covered

19 relevant lines. 15 lines covered and 4 lines missed.
    
  1. 1 class SearchService
  2. 1 def self.reindex_everything
  3. [
  4. Discussion.pg_search_insert_statement,
  5. Comment.pg_search_insert_statement,
  6. Poll.pg_search_insert_statement,
  7. Stance.pg_search_insert_statement,
  8. Outcome.pg_search_insert_statement
  9. ].each do |statement|
  10. ActiveRecord::Base.connection.execute(statement)
  11. end
  12. end
  13. 1 def self.reindex_by_author_id(author_id)
  14. 11 PgSearch::Document.where(author_id: author_id).delete_all
  15. [
  16. 11 Discussion.pg_search_insert_statement(author_id: author_id),
  17. Comment.pg_search_insert_statement(author_id: author_id),
  18. Poll.pg_search_insert_statement(author_id: author_id),
  19. Stance.pg_search_insert_statement(author_id: author_id),
  20. Outcome.pg_search_insert_statement(author_id: author_id),
  21. ].each do |statement|
  22. 55 ActiveRecord::Base.connection.execute(statement)
  23. end
  24. end
  25. 1 def self.reindex_by_discussion_id(discussion_id)
  26. 18 PgSearch::Document.where(discussion_id: discussion_id).delete_all
  27. [
  28. 18 Discussion.pg_search_insert_statement(id: discussion_id),
  29. Comment.pg_search_insert_statement(discussion_id: discussion_id),
  30. Poll.pg_search_insert_statement(discussion_id: discussion_id),
  31. Stance.pg_search_insert_statement(discussion_id: discussion_id),
  32. Outcome.pg_search_insert_statement(discussion_id: discussion_id),
  33. ].each do |statement|
  34. 90 ActiveRecord::Base.connection.execute(statement)
  35. end
  36. end
  37. 1 def self.reindex_by_poll_id(poll_id)
  38. 27 PgSearch::Document.where(poll_id: poll_id).delete_all
  39. [
  40. 27 Poll.pg_search_insert_statement(id: poll_id),
  41. Stance.pg_search_insert_statement(poll_id: poll_id),
  42. Outcome.pg_search_insert_statement(poll_id: poll_id),
  43. ].each do |statement|
  44. 81 ActiveRecord::Base.connection.execute(statement)
  45. end
  46. end
  47. 1 def self.reindex_by_comment_id(comment_id)
  48. # Comment.find(comment_id).update_pg_search_document
  49. PgSearch::Document.where(searchable_type: 'Comment', searchable_id: comment_id).delete_all
  50. ActiveRecord::Base.connection.execute(Comment.pg_search_insert_statement(id: comment_id))
  51. end
  52. end

app/services/sequence_service.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class SequenceService
  2. 1 def self.seq_present?(key, id)
  3. 1699 ActiveRecord::Base.connection.execute(
  4. "SELECT 0 FROM partition_sequences where key = '#{key}' and id = #{id}"
  5. ).first.present?
  6. end
  7. 1 def self.create_seq!(key, id, start)
  8. 1034 ActiveRecord::Base.connection.execute(
  9. "INSERT INTO partition_sequences (key, id, counter) VALUES ('#{key}', #{id}, #{start}) ON CONFLICT DO NOTHING"
  10. )
  11. end
  12. 1 def self.next_seq!(key, id)
  13. 1699 ActiveRecord::Base.connection.execute(
  14. "UPDATE partition_sequences SET counter = counter + 1 WHERE key = '#{key}' AND id = #{id} RETURNING (counter)"
  15. )[0]["counter"]
  16. end
  17. 1 def self.drop_seq!(key, id)
  18. 96 ActiveRecord::Base.connection.execute(
  19. "DELETE FROM partition_sequences WHERE key = '#{key}' AND id = #{id}"
  20. )
  21. end
  22. end

app/services/stance_service.rb

82.61% lines covered

46 relevant lines. 38 lines covered and 8 lines missed.
    
  1. 1 class StanceService
  2. 1 def self.create(stance:, actor:)
  3. 24 actor.ability.authorize!(:vote_in, stance.poll)
  4. 18 stance.participant = actor
  5. 18 stance.cast_at ||= Time.zone.now
  6. 18 stance.revoked_at = nil
  7. 18 stance.revoker_id = nil
  8. 18 stance.save!
  9. 13 stance.poll.update_counts!
  10. 13 event = Events::StanceCreated.publish!(stance)
  11. 13 event
  12. end
  13. 1 def self.uncast(stance:, actor:)
  14. 2 actor.ability.authorize!(:uncast, stance)
  15. 1 new_stance = stance.build_replacement
  16. 1 Stance.transaction do
  17. 1 stance.update_columns(latest: false)
  18. 1 new_stance.save!
  19. end
  20. 1 new_stance.poll.update_counts!
  21. end
  22. 1 def self.update(stance: , actor: , params: )
  23. 52 actor.ability.authorize!(:update, stance)
  24. 52 is_update = !!stance.cast_at
  25. 52 new_stance = stance.build_replacement
  26. 52 new_stance.assign_attributes_and_files(params)
  27. 52 event = Event.where(eventable: stance, discussion_id: stance.poll.discussion_id).order('id desc').first
  28. 52 if is_update && stance.option_scores != new_stance.build_option_scores && event && event.child_count > 0
  29. # they've changed their position and there were replies! create a new stance, so that discussion threads make sense
  30. new_stance.cast_at = Time.zone.now
  31. Stance.transaction do
  32. stance.update_columns(latest: false)
  33. new_stance.save!
  34. end
  35. new_stance.poll.update_counts!
  36. MessageChannelService.publish_models([stance], group_id: stance.poll.group_id)
  37. Events::StanceCreated.publish!(new_stance)
  38. else
  39. 52 stance.stance_choices = []
  40. 52 stance.assign_attributes_and_files(params)
  41. 52 stance.cast_at ||= Time.zone.now
  42. 52 stance.revoked_at = nil
  43. 52 stance.revoker_id = nil
  44. 52 stance.save!
  45. 52 stance.poll.update_counts!
  46. 52 if is_update
  47. Events::StanceUpdated.publish!(stance)
  48. else
  49. 52 Events::StanceCreated.publish!(stance)
  50. end
  51. end
  52. end
  53. 1 def self.redeem(stance:, actor:)
  54. 2 return if Stance.latest.where(participant_id: actor.id, poll_id: stance.poll_id).exists?
  55. 2 return unless Stance.redeemable_by(actor).where(id: stance.id).exists?
  56. 1 stance.update(participant: actor, accepted_at: Time.zone.now)
  57. end
  58. # def self.destroy(stance:, actor:)
  59. # actor.ability.authorize! :destroy, stance
  60. # stance.destroy
  61. # EventBus.broadcast 'stance_destroy', stance, actor
  62. # end
  63. end

app/services/tag_service.rb

95.16% lines covered

62 relevant lines. 59 lines covered and 3 lines missed.
    
  1. 1 class TagService
  2. 1 def self.create(tag:, actor:)
  3. 1 actor.ability.authorize! :create, tag
  4. 1 return false unless tag.valid?
  5. 1 tag.save!
  6. 1 EventBus.broadcast 'tag_create', tag, actor
  7. 1 MessageChannelService.publish_models([tag], group_id: tag.group.id)
  8. 1 tag
  9. end
  10. 1 def self.update(tag:, params:, actor:)
  11. 5 actor.ability.authorize! :update, tag
  12. 5 UpdateTagWorker.new.perform(tag.group_id, tag.name, params[:name].strip, params[:color])
  13. 5 tag.reload
  14. 5 MessageChannelService.publish_models([tag], group_id: tag.group.id)
  15. 5 EventBus.broadcast 'tag_update', tag, actor
  16. 5 tag
  17. end
  18. 1 def self.destroy(tag:, actor:)
  19. actor.ability.authorize! :destroy, tag
  20. DestroyTagWorker.perform_async(tag.group_id, tag.name)
  21. EventBus.broadcast 'tag_destroy', tag, actor
  22. end
  23. 1 def self.apply_colors(group_id)
  24. 2531 group_ids = Group.find(group_id).parent_or_self.id_and_subgroup_ids
  25. 2531 Tag.where(group_id: group_id, color: nil).each do |tag|
  26. 286 if parent_tag = Tag.where(group_id: group_ids, name: tag.name).where.not(color: nil).first
  27. 13 tag.update_columns(color: parent_tag.color)
  28. else
  29. 273 tag.update_columns(color: Tag::COLORS.sample)
  30. end
  31. end
  32. end
  33. 1 def self.update_group_and_org_tags(group_id)
  34. 5172 update_group_tags(group_id)
  35. 5172 update_org_tagging_counts(Group.find(group_id).parent_or_self.id)
  36. end
  37. 1 def self.update_group_tags(group_id)
  38. 5186 return unless group = Group.find_by(id: group_id)
  39. 5186 names = (group.discussions.kept.select(:tags).pluck(:tags).flatten +
  40. group.polls.kept.select(:tags).pluck(:tags).flatten)
  41. 5186 return if names.empty?
  42. 1270 counts = {}
  43. 1270 names.map(&:downcase).each do |dname|
  44. 2241 counts[dname] ||= 0
  45. 2241 counts[dname] += 1
  46. end
  47. 1270 group.tags.where.not(name: counts.keys).update_all(taggings_count: 0)
  48. 1270 present = Tag.where(group_id: group_id, name: counts.keys).pluck(:name).map(&:downcase)
  49. 1270 missing = counts.keys - present
  50. 1270 Tag.where(group_id: group_id, name: present).each do |tag|
  51. 1871 tag.update_column(:taggings_count, counts[tag.name.downcase])
  52. end
  53. 1270 missing.each do |dname|
  54. 285 Tag.insert({group_id: group_id,
  55. 427 name: names.find {|name| name.downcase == dname},
  56. taggings_count: counts[dname]})
  57. end
  58. 1270 apply_colors(group_id)
  59. end
  60. 1 def self.update_org_tagging_counts(group_id)
  61. 5177 return unless group = Group.find_by(id: group_id)
  62. 5177 group_ids = group.id_and_subgroup_ids
  63. 5177 names = Tag.where(group_id: group_ids).pluck(:name).uniq
  64. 5177 return if names.empty?
  65. 1261 counts = {}
  66. 1261 Tag.where(group_id: group_ids).pluck(:name).map(&:downcase).uniq.map do |dname|
  67. 2141 counts[dname] = Tag.where(group_id: group_ids, name: dname).sum(:taggings_count)
  68. end
  69. 1261 group.tags.where.not(name: counts.keys).update_all(org_taggings_count: 0)
  70. 1261 present = Tag.where(group_id: group_id, name: names).pluck(:name).map(&:downcase)
  71. 1261 missing = counts.keys - present
  72. 1261 Tag.where(group_id: group_id, name: present).each do |tag|
  73. 2140 tag.update_column(:org_taggings_count, counts[tag.name.downcase])
  74. end
  75. 1261 missing.each do |dname|
  76. 1 Tag.insert({group_id: group_id,
  77. 2 name: names.find {|name| name.downcase == dname},
  78. org_taggings_count: counts[dname]})
  79. end
  80. 1261 apply_colors(group_id)
  81. end
  82. end

app/services/task_service.rb

96.61% lines covered

59 relevant lines. 57 lines covered and 2 lines missed.
    
  1. 1 class TaskService
  2. 1 def self.send_task_reminders(time = Time.now.utc.at_beginning_of_hour)
  3. 1 Task.not_done.where(remind_at: time).each do |task|
  4. 1 task.users.each do |user|
  5. 1 TaskMailer.task_due_reminder(user, task).deliver_later
  6. end
  7. end
  8. end
  9. 1 def self.update_done(task, actor, done)
  10. 5 task.done = done
  11. 5 task.done_at = (done && Time.now) || nil
  12. 5 task.doer = (done && actor) || nil
  13. 5 record = task.record
  14. 5 doc = Nokogiri::HTML::DocumentFragment.parse(record.body)
  15. 5 doc.css("li[data-uid='#{task.uid}']").each do |li|
  16. 5 li['data-checked'] = done ? 'true' : 'false'
  17. end
  18. 5 record.body = doc.to_html
  19. 5 record.save!
  20. 5 task.save!
  21. 5 if record.group_id
  22. 5 MessageChannelService.publish_models([record], group_id: record.group.id)
  23. end
  24. 5 if record.respond_to?(:guests)
  25. 5 record.guests.find_each do |user|
  26. MessageChannelService.publish_models([record], user_id: user.id)
  27. end
  28. end
  29. end
  30. 1 def self.rewrite_uids(text)
  31. 76386 node = Nokogiri::HTML::fragment(text)
  32. 76386 uids = []
  33. 76386 node.search('li[data-type="taskItem"]').each do |el|
  34. 10 if uids.include?(el['data-uid'].to_i)
  35. el['data-uid'] = (rand() * 100000000).to_i
  36. end
  37. 10 uids.push el['data-uid'].to_i
  38. end
  39. 76386 node.to_html
  40. end
  41. 1 def self.parse_and_update(model, field)
  42. 76385 update_model(model, parse_tasks(model[field], model.author))
  43. end
  44. 1 def self.parse_tasks(rich_text, author)
  45. 76394 Nokogiri::HTML::fragment(rich_text).search('li[data-type="taskItem"]').map do |el|
  46. 18 identifiers = Nokogiri::HTML::fragment(el).
  47. search("span[data-mention-id]").map do |el|
  48. 14 el['data-mention-id']
  49. end
  50. 32 usernames = identifiers.filter { |id_or_username| id_or_username.to_i.to_s != id_or_username }
  51. 32 user_ids = identifiers.filter { |id_or_username| id_or_username.to_i.to_s == id_or_username }
  52. 18 remind = (el['data-remind'].present? ? el['data-remind'].to_i : nil)
  53. 18 due_on = el['data-due-on'].to_s.to_date
  54. 18 remind_at = (due_on && remind) ? ("#{el['data-due-on']} 06:00".in_time_zone(author.time_zone) - remind.day) : nil
  55. {
  56. 18 uid: el['data-uid'].to_i,
  57. name: el.text,
  58. user_ids: user_ids,
  59. usernames: usernames,
  60. due_on: el['data-due-on'].to_s.to_date,
  61. remind: remind,
  62. remind_at: remind_at,
  63. done: el['data-checked'] == 'true',
  64. 18 author_id: (el['data-author-id'] && el['data-author-id'].to_i) || author.id
  65. }
  66. end
  67. end
  68. 1 def self.update_model(model, tasks_data)
  69. 76408 uids = tasks_data.map {|t| t[:uid] }
  70. 76392 existing_uids = model.tasks.pluck(:uid)
  71. 76392 new_uids = uids - existing_uids
  72. 76392 removed_uids = existing_uids - uids
  73. # delete tasks which are not mentioned by uid
  74. # TODO maybe notify people if a task is deleted. or mark it as discarded
  75. 76392 model.tasks.where(uid: removed_uids).destroy_all
  76. # update existing tasks
  77. 76392 model.tasks.where(uid: existing_uids).each do |task|
  78. 12 data = tasks_data.find { |t| t[:uid] == task.uid }
  79. 6 mentioned_users = model.members.where('users.id in (:ids) or users.username in (:names)',
  80. ids: data[:user_ids],
  81. names: data[:usernames])
  82. 6 new_users = mentioned_users.where('users.id not in (?)', task.users.pluck(:id))
  83. 6 removed_users = task.users.where('users.id not in (?)', mentioned_users.pluck(:id))
  84. 6 task.update!(name: data[:name],
  85. due_on: data[:due_on],
  86. users: mentioned_users,
  87. done: data[:done],
  88. remind: data[:remind],
  89. remind_at: data[:remind_at],
  90. 6 done_at: (!task.done && data[:done]) ? Time.now : task.done_at,
  91. author: model.members.find_by('users.id': data[:author_id]) || model.author)
  92. end
  93. # create tasks which dont yet exist
  94. 76408 tasks_data.filter{|t| new_uids.include?(t[:uid]) }.each do |data|
  95. 10 users = model.members.where('users.id in (:ids) or users.username in (:names)',
  96. ids: data[:user_ids],
  97. names: data[:usernames])
  98. 10 model.tasks.create(
  99. uid: data[:uid],
  100. name: data[:name],
  101. due_on: data[:due_on],
  102. remind: data[:remind],
  103. remind_at: data[:remind_at],
  104. users: users,
  105. done: data[:done],
  106. 10 done_at: (data[:done] ? Time.now : nil),
  107. author: model.members.find_by('users.id': data[:author_id]) || model.author
  108. )
  109. end
  110. end
  111. end

app/services/throttle_service.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module ThrottleService
  2. 1 class LimitReached < StandardError
  3. end
  4. 1 def self.reset!(per)
  5. 2 CACHE_REDIS_POOL.with do |client|
  6. 3 client.scan_each(match: "THROTTLE-#{per.upcase}*") { |key| client.del(key) }
  7. end
  8. end
  9. 1 def self.can?(key: 'default-key', id: 1, max: 100 , inc: 1, per: 'hour')
  10. 1236 raise "Throttle per is not hour or day: #{per}" unless ['hour', 'day'].include? per.to_s
  11. 1236 k = "THROTTLE-#{per.upcase}-#{key}-#{id}"
  12. 1236 Redis::Counter.new(k).increment(inc)
  13. 1236 Redis::Counter.new(k).value <= ENV.fetch('THROTTLE_MAX_'+key, max)
  14. end
  15. 1 def self.limit!(key: 'default-key', id: 1, max: 100 , inc: 1, per: 'hour')
  16. 1216 if can?(key: key, id: id, max: max, inc: inc, per: per)
  17. 1215 return true
  18. else
  19. 1 raise ThrottleService::LimitReached.new "Throttled! #{key}-#{id}"
  20. end
  21. end
  22. end

app/services/transcription_service.rb

66.67% lines covered

6 relevant lines. 4 lines covered and 2 lines missed.
    
  1. 1 class TranscriptionService
  2. 1 def self.available?
  3. 21 ENV['OPENAI_API_KEY'].present?
  4. end
  5. 1 def self.transcribe(file)
  6. client = OpenAI::Client.new(access_token: ENV.fetch('OPENAI_API_KEY'))
  7. client.audio.transcribe(
  8. parameters: {
  9. model: "whisper-1",
  10. file: file,
  11. response_format: :verbose_json,
  12. }
  13. )
  14. end
  15. end

app/services/translation_service.rb

20.41% lines covered

49 relevant lines. 10 lines covered and 39 lines missed.
    
  1. 1 require "google/cloud/translate"
  2. 1 class TranslationService
  3. 1 extend LocalesHelper
  4. 1 GOOGLE_LOCALES = %w[af sq am ar hy as ay az bm eu be bn bho bs bg ca ceb zh-CN zh zh-TW co hr cs da dv doi nl en eo et ee fil fi fr fy gl ka de el gn gu ht ha haw he iw hi hmn hu is ig ilo id ga it ja jv jw kn kk km rw gom ko kri ku ckb ky lo la lv ln lt lg lb mk mai mg ms ml mt mi mr mni-Mtei lus mn my ne no ny or om ps fa pl pt pa qu ro ru sm sa gd nso sr st sn sd si sk sl so es su sw sv tl tg ta tt te th ti ts tr tk ak uk ur ug uz vi cy xh yi yo zu]
  5. 1 def self.locale_for_google(locale)
  6. locale = locale.downcase.gsub("_", "-")
  7. return locale if GOOGLE_LOCALES.include?(locale)
  8. locale.split("-")[0]
  9. end
  10. 1 def self.create(model:, to:)
  11. locale = locale_for_google(to)
  12. translation = model.translations.find_by(language: locale) ||
  13. Translation.new(translatable: model, language: locale, fields: {})
  14. if translation.new_record? || ((translation.updated_at || translation.created_at) < (model.updated_at || model.created_at || 5.years.ago))
  15. service = Google::Cloud::Translate.translation_v2_service
  16. model.class.translatable_fields.each do |field|
  17. next if model.send(field).blank?
  18. translation.fields[field.to_s] = service.translate(model.send(field), to: locale)
  19. end
  20. translation.save!
  21. end
  22. translation
  23. end
  24. 1 def self.available?
  25. 57501 ENV['TRANSLATE_CREDENTIALS'].present?
  26. end
  27. 1 def self.translate_group_content!(group, locale, cache_only = false)
  28. return if locale == 'en'
  29. translate_group_record(group, group, locale, cache_only)
  30. group.discussions.each do |discussion|
  31. translate_group_record(group, discussion, locale, cache_only)
  32. end
  33. group.polls.each do |poll|
  34. translate_group_record(group, poll, locale, cache_only)
  35. poll.outcomes.each do |outcome|
  36. translate_group_record(group, outcome, locale, cache_only)
  37. end
  38. poll.poll_options.each do |poll_option|
  39. if poll.poll_option_name_format != 'plain'
  40. translate_group_record(group, poll_option, locale, cache_only, ignore: 'name')
  41. else
  42. translate_group_record(group, poll_option, locale, cache_only)
  43. end
  44. end
  45. poll.stances.each do |stance|
  46. translate_group_record(group, stance, locale, cache_only)
  47. end
  48. end
  49. group.comments.each do |comment|
  50. translate_group_record(group, comment, locale, cache_only)
  51. end
  52. group.tags.each do |tag|
  53. translate_group_record(group, tag, locale, cache_only)
  54. end
  55. end
  56. 1 def self.translate_group_record(group, record, locale, cache_only = false, ignore: [])
  57. translate_record = if source_record_id = group.info.dig('source_record_ids', "#{record.class.to_s}-#{record.id}")
  58. record.class.find(source_record_id)
  59. else
  60. record
  61. end
  62. translation = TranslationService.create(model: translate_record, to: locale)
  63. return if cache_only
  64. translation.fields.each do |pair|
  65. next if ignore.include?(pair[0])
  66. record.update_attribute(pair[0], pair[1])
  67. end
  68. record.update_content_locale if record.has_attribute?(:content_locale)
  69. end
  70. end

app/services/user_service.rb

94.64% lines covered

56 relevant lines. 53 lines covered and 3 lines missed.
    
  1. 1 class UserService
  2. 1 class EmailTakenError < StandardError
  3. end
  4. 1 def self.create(params:)
  5. 10 if User.where(email_verified: true, email: params[:email]).exists?
  6. 1 raise UserService::EmailTakenError.new(email: params[:email])
  7. end
  8. 9 user = User.where(email_verified: false, email: params[:email]).first_or_create
  9. 9 user.attributes = params.slice(:name, :email, :recaptcha, :legal_accepted, :email_newsletter)
  10. 9 user.require_valid_signup = true
  11. 9 user.require_recaptcha = true
  12. 9 user.save
  13. 9 user
  14. rescue ActiveRecord::RecordNotUnique
  15. retry
  16. end
  17. 1 def self.verify(user: )
  18. 129 return user if user.email_verified?
  19. 8 user = User.verified.find_by(email: user.email) || user.tap{ |u| u.update(email_verified: true) }
  20. 4 if user.email_newsletter?
  21. GenericWorker.perform_async('NewsletterService', 'subscribe', user.name, user.email)
  22. end
  23. 4 user
  24. end
  25. 1 def self.deactivate(user:, actor:)
  26. 2 actor.ability.authorize! :deactivate, user
  27. 2 DeactivateUserWorker.perform_async(user.id, actor.id)
  28. end
  29. 1 def self.redact(user:, actor:)
  30. 4 actor.ability.authorize! :redact, user
  31. 4 RedactUserWorker.perform_async(user.id, actor.id)
  32. end
  33. 1 def self.reactivate(user_id)
  34. 1 user = User.find(user_id)
  35. 1 deactivated_at = user.deactivated_at
  36. 1 Membership.where(user_id: user.id, revoked_at: deactivated_at).update_all(revoked_at: nil, revoker_id: nil)
  37. 1 group_ids = Membership.where(user_id: user.id).pluck(:group_id)
  38. 1 Group.where(id: group_ids).map(&:update_memberships_count)
  39. 1 user.update(deactivated_at: nil)
  40. 1 GenericWorker.perform_async('SearchService', 'reindex_by_author_id', user.id)
  41. end
  42. 1 def self.set_volume(user:, actor:, params:)
  43. 2 actor.ability.authorize! :update, user
  44. 2 user.update!(default_membership_volume: params[:volume])
  45. 2 if params[:apply_to_all]
  46. 1 user.memberships.update_all(volume: Membership.volumes[params[:volume]])
  47. 1 user.discussion_readers.update_all(volume: Membership.volumes[params[:volume]])
  48. 1 user.stances.update_all(volume: Membership.volumes[params[:volume]])
  49. end
  50. 2 EventBus.broadcast('user_set_volume', user, actor, params)
  51. end
  52. 1 def self.update(user:, actor:, params:)
  53. 3 actor.ability.authorize! :update, user
  54. 3 user.assign_attributes_and_files(params)
  55. 3 return false unless user.valid?
  56. 3 user.save!
  57. 3 EventBus.broadcast('user_update', user, actor, params)
  58. 3 GenericWorker.perform_async('SearchService', 'reindex_by_author_id', user.id) if user.name_previously_changed?
  59. end
  60. 1 def self.save_experience(user:, actor:, params:)
  61. 2 actor.ability.authorize! :update, user
  62. 1 name = params[:experience]
  63. 1 value = if params.has_key?(:remove_experience)
  64. nil
  65. else
  66. 1 params.fetch(:value, true)
  67. end
  68. 1 user.experiences[name] = value
  69. 1 user.save!
  70. 1 EventBus.broadcast('user_save_experience', user, actor, params)
  71. end
  72. end

app/validators/email_validator.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. class EmailValidator < ActiveModel::EachValidator
  2. def validate_each(record, attribute, value)
  3. unless value =~ Devise.email_regexp
  4. record.errors.add(attribute, "Not a valid email")
  5. end
  6. end
  7. end

app/workers/accept_membership_worker.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class AcceptMembershipWorker
  2. include Sidekiq::Worker
  3. def perform(membership_id, user_id)
  4. return unless membership = Membership.pending.find_by(id: membership_id)
  5. user = User.find(user_id)
  6. MembershipService.redeem(membership: membership, actor: user, notify: false)
  7. end
  8. end

app/workers/add_group_id_to_documents_worker.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class AddGroupIdToDocumentsWorker
  2. include Sidekiq::Worker
  3. def perform
  4. Document.where(group_id: nil).find_each do |document|
  5. document.update_column(:group_id, document.model.group_id) if document.model && document.model.respond_to?(:group_id)
  6. end
  7. end
  8. end

app/workers/add_heading_ids_worker.rb

0.0% lines covered

19 relevant lines. 0 lines covered and 19 lines missed.
    
  1. class AddHeadingIdsWorker
  2. include Sidekiq::Worker
  3. def perform()
  4. {
  5. Discussion => 'description',
  6. Comment => 'body',
  7. Poll => 'details',
  8. Outcome => 'statement',
  9. Stance => 'reason',
  10. Group => 'description'
  11. }.each_pair do |model, field|
  12. rel = model.where("#{field}_format": 'html').where("#{field} is not null and #{field} != ''")
  13. puts "Updating #{rel.count} #{model.to_s.pluralize}"
  14. rel.find_each do |r|
  15. model.where(id: r.id).update_all(field => HasRichText::add_heading_ids(r[field]))
  16. end
  17. end
  18. end
  19. end

app/workers/announce_discussion_worker.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. class AnnounceDiscussionWorker
  2. include Sidekiq::Worker
  3. def perform(discussion_id, actor_id, params)
  4. DiscussionService.invite(
  5. discussion: Discussion.find(discussion_id),
  6. actor: User.find(actor_id),
  7. params: params.with_indifferent_access
  8. )
  9. end
  10. end

app/workers/append_transcript_worker.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. class AppendTranscriptWorker
  2. include Sidekiq::Worker
  3. def perform(blob_id)
  4. blob = ActiveStorage::Blob.find(blob_id)
  5. text = blob.metadata['text']
  6. blob.attachments.each do |attachment|
  7. record = attachment.record
  8. record.body = record.body + "<p>#{text}</p>"
  9. record.save!
  10. MessageChannelService.publish_models(Array(record), group_id: record.group_id)
  11. end
  12. end
  13. end

app/workers/attach_document_worker.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class AttachDocumentWorker
  2. include Sidekiq::Worker
  3. def perform(document_id)
  4. d = Document.find(document_id)
  5. return if d.file.attached?
  6. s3 = ActiveStorage::Blob.service
  7. path = URI(d.url).path.gsub("/attachments", "attachments")
  8. obj = s3.bucket.object(path)
  9. params = {filename: obj.key, content_type: obj.content_type, byte_size: obj.size, checksum: obj.etag.gsub('"',"") }
  10. blob = ActiveStorage::Blob.create_before_direct_upload!(**params)
  11. blob.key = obj.key
  12. d.file.attach(blob)
  13. end
  14. end

app/workers/close_expired_poll_worker.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class CloseExpiredPollWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(poll_id)
  4. 15 poll = Poll.find(poll_id)
  5. 15 return if poll.closed_at
  6. 15 PollService.do_closing_work(poll: poll)
  7. 15 Events::PollExpired.publish!(poll)
  8. end
  9. end

app/workers/convert_discussion_templates_worker.rb

0.0% lines covered

35 relevant lines. 0 lines covered and 35 lines missed.
    
  1. class ConvertDiscussionTemplatesWorker
  2. include Sidekiq::Worker
  3. def perform
  4. Discussion.where(template: true).each do |discussion|
  5. template = DiscussionTemplate.new(discussion.slice(
  6. :group_id,
  7. :author_id,
  8. :title,
  9. :description,
  10. :description_format,
  11. :tags,
  12. :max_depth,
  13. :newest_first,
  14. :content_locale,
  15. :link_previews,
  16. :discarded_at,
  17. :discarded_by,
  18. :created_at,
  19. :updated_at,
  20. :attachments))
  21. template.process_name = discussion.title
  22. template.source_discussion_id = discussion.id
  23. template.save!
  24. discussion.files.attachments.update_all(
  25. record_type: 'DiscussionTemplate',
  26. record_id: template.id
  27. )
  28. discussion.image_files.attachments.update_all(
  29. record_type: 'DiscussionTemplate',
  30. record_id: template.id
  31. )
  32. discussion.discard! if discussion.kept?
  33. end
  34. end
  35. end

app/workers/convert_poll_stances_in_discussion_worker.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. class ConvertPollStancesInDiscussionWorker
  2. include Sidekiq::Worker
  3. sidekiq_options queue: :low, retry: false
  4. def perform(poll_id)
  5. poll = Poll.find(poll_id)
  6. return if !poll.discussion_id
  7. return if poll.stances_in_discussion
  8. poll.update_attribute(:stances_in_discussion, true)
  9. stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
  10. Stance.where(id: stance_ids).each do |stance|
  11. stance.create_missing_created_event! if stance.created_event.nil?
  12. end
  13. Event.where(kind: 'stance_created', eventable_id: stance_ids, discussion_id: nil).update_all(discussion_id: poll.discussion_id)
  14. EventService.repair_thread(poll.discussion_id)
  15. end
  16. end

app/workers/deactivate_user_worker.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class DeactivateUserWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(user_id, actor_id)
  4. 3 user = User.find(user_id)
  5. 3 deactivated_at = DateTime.now
  6. 3 group_ids = Membership.active.where(user_id: user_id).pluck(:group_id)
  7. 3 User.transaction do
  8. 3 MembershipService.revoke_by_id(group_ids, user_id, actor_id, deactivated_at)
  9. 3 user.update(deactivated_at: deactivated_at, deactivator_id: actor_id)
  10. 3 MembershipRequest.where(requestor_id: user_id, responded_at: nil).destroy_all
  11. end
  12. 3 SearchService.reindex_by_author_id(user.id)
  13. end
  14. end

app/workers/destroy_discussion_worker.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class DestroyDiscussionWorker
  2. include Sidekiq::Worker
  3. def perform(discussion_id)
  4. Discussion.discarded.find(discussion_id).destroy!
  5. end
  6. end

app/workers/destroy_group_worker.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class DestroyGroupWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(group_id)
  4. 1 Group.archived.find_by(id: group_id).try(:destroy!)
  5. end
  6. end

app/workers/destroy_record_worker.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class DestroyRecordWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(class_name, record_id)
  4. 1 class_name.constantize.find(record_id).destroy!
  5. end
  6. end

app/workers/destroy_tag_worker.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class DestroyTagWorker
  2. include Sidekiq::Worker
  3. def perform(group_id, name)
  4. group = Group.find(group_id)
  5. group_ids = group.id_and_subgroup_ids
  6. Tag.transaction do
  7. Tag.where(group_id: group_ids, name: name).delete_all
  8. Discussion.where(group_id: group_ids).where.contains(tags: [name]).find_each do |d|
  9. d.update_column(:tags, d.tags - Array(name))
  10. end
  11. Poll.where(group_id: group_ids).where.contains(tags: [name]).find_each do |p|
  12. p.update_column(:tags, p.tags - Array(name))
  13. end
  14. end
  15. TagService.update_org_tagging_counts(group.parent_or_self.id)
  16. end
  17. end

app/workers/destroy_user_worker.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class DestroyUserWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(user_id)
  4. 5 ActiveRecord::Base.transaction do
  5. # invited_user_ids = []
  6. # invited_user_ids.concat DiscussionReader.where(inviter_id: user_id).pluck(:user_id)
  7. # invited_user_ids.concat Membership.where(inviter_id: user_id).pluck(:user_id)
  8. # invited_user_ids.concat Stance.where(inviter_id: user_id).pluck(:participant_id)
  9. # invited_user_ids = User.where(email_verified: false).where(id: invited_user_ids).pluck(:id)
  10. #
  11. # event_ids = Event.where(user_id: user_id).pluck(:id)
  12. # Notification.where(event_id: event_ids).delete_all
  13. # DiscussionReader.where(inviter_id: user_id).delete_all
  14. # Membership.where(inviter_id: user_id).delete_all
  15. # User.where(id: invited_user_ids).delete_all
  16. # Event.where(user_id: user_id).delete_all
  17. #
  18. #
  19. # User.find(user_id).destroy!
  20. 5 User.find(user_id).destroy!
  21. end
  22. end
  23. end

app/workers/download_attachment_worker.rb

0.0% lines covered

6 relevant lines. 0 lines covered and 6 lines missed.
    
  1. class DownloadAttachmentWorker
  2. include Sidekiq::Worker
  3. def perform(record, new_id)
  4. GroupExportService.download_attachment(record, new_id)
  5. end
  6. end

app/workers/fix_stances_missing_from_threads_worker.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. class FixStancesMissingFromThreadsWorker
  2. include Sidekiq::Worker
  3. def perform
  4. stance_ids = Event.where("discussion_id is not null").where(eventable_type: 'Stance').pluck(:eventable_id)
  5. poll_ids = Stance.where(id: stance_ids).pluck(:poll_id).uniq
  6. Stance.joins(:poll).
  7. where("polls.discussion_id is not null").
  8. where("reason is not null").
  9. where("stances.poll_id NOT IN (?)", poll_ids).find_each do |s|
  10. s.create_missing_created_event! if s.add_to_discussion?
  11. end
  12. end
  13. end

app/workers/generic_worker.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class GenericWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(class_name, method_name, arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, arg5 = nil)
  4. 12211 class_name.constantize.send(method_name, *([arg1, arg2, arg3, arg4, arg5].compact))
  5. end
  6. end

app/workers/geo_location_worker.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. class GeoLocationWorker
  2. include Sidekiq::Worker
  3. def perform
  4. db_filename = Rails.root.join('public', 'GeoLite2-Country.mmdb').to_s
  5. unless File.exist? db_filename
  6. # from https://github.com/P3TERX/GeoLite.mmdb
  7. download = URI.parse("https://git.io/GeoLite2-Country.mmdb").open
  8. IO.copy_stream(download, db_filename)
  9. puts "downloaded maxmind db"
  10. end
  11. db = MaxMindDB.new(db_filename)
  12. User.where(country: nil).where("current_sign_in_ip is not null").find_each do |user|
  13. record = db.lookup(user.current_sign_in_ip.to_s)
  14. next unless record.found?
  15. user.update_columns(country: record.country.name)
  16. end
  17. end
  18. end

app/workers/group_export_csv_worker.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class GroupExportCsvWorker
  2. include Sidekiq::Worker
  3. def perform(group_id, actor_id)
  4. actor = User.find(actor_id)
  5. group = Group.find(group_id)
  6. csv = GroupExporter.new(group).to_csv
  7. filename = "#{group.full_name} CSV export #{DateTime.now.iso8601}".parameterize+".csv"
  8. document = Document.new(author: actor, title: filename)
  9. document.file.attach(io: StringIO.new(csv), filename: filename)
  10. document.save!
  11. UserMailer.group_export_ready(actor.id, group.full_name, document.id).deliver
  12. DestroyRecordWorker.perform_at(1.week.from_now, 'Document', document.id)
  13. end
  14. end

app/workers/group_export_worker.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class GroupExportWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(group_ids, group_name, actor_id)
  4. 1 actor = User.find_by!(id:actor_id)
  5. 1 groups = Group.where(id: group_ids)
  6. 1 filename = GroupExportService.export(groups, group_name)
  7. 1 document = Document.new(author: actor, title: filename)
  8. 1 document.file.attach(io: File.open(filename), filename: filename)
  9. 1 document.save!
  10. 1 UserMailer.group_export_ready(actor.id, group_name, document.id).deliver
  11. 1 DestroyRecordWorker.perform_at(1.week.from_now, 'Document', document.id)
  12. end
  13. end

app/workers/migrate_guest_on_discussion_readers_and_stances.rb

0.0% lines covered

18 relevant lines. 0 lines covered and 18 lines missed.
    
  1. class MigrateGuestOnDiscussionReadersAndStances
  2. include Sidekiq::Worker
  3. def perform(group_id)
  4. member_ids = Membership.active.where(group_id: group_id).pluck(:user_id)
  5. DiscussionReader
  6. .active.joins(:discussion)
  7. .where('discussions.group_id': group_id)
  8. .where.not(inviter_id: nil)
  9. .where.not(user_id: member_ids)
  10. .update_all(guest: true)
  11. Stance
  12. .latest.joins(:poll)
  13. .where('polls.group_id': group_id)
  14. .where.not(inviter_id: nil)
  15. .where.not(participant_id: member_ids)
  16. .update_all(guest: true)
  17. end
  18. end

app/workers/migrate_poll_templates_worker.rb

0.0% lines covered

51 relevant lines. 0 lines covered and 51 lines missed.
    
  1. class MigratePollTemplatesWorker
  2. include Sidekiq::Worker
  3. def perform
  4. Poll.where(template: true).where("group_id is not null").find_each do |p|
  5. pt = PollTemplate.new
  6. [
  7. :group_id,
  8. :author_id,
  9. :poll_type,
  10. :process_name,
  11. :process_subtitle,
  12. :title,
  13. :details,
  14. :details_format,
  15. :anonymous,
  16. :specified_voters_only,
  17. :notify_on_closing_soon,
  18. :content_locale,
  19. :shuffle_options,
  20. :hide_results,
  21. :chart_type,
  22. :min_score,
  23. :max_score,
  24. :minimum_stance_choices,
  25. :maximum_stance_choices,
  26. :dots_per_person,
  27. :reason_prompt,
  28. :stance_reason_required,
  29. :limit_reason_length,
  30. :agree_target,
  31. :created_at,
  32. :updated_at,
  33. :meeting_duration,
  34. :can_respond_maybe,
  35. :poll_option_name_format,
  36. :tags].each do |field|
  37. pt[field] = p.send(field)
  38. end
  39. pt[:default_duration_in_days] = 7
  40. pt.poll_options = p.poll_options.map do |o|
  41. {
  42. name: o.name,
  43. meaning: o.meaning,
  44. prompt: o.prompt,
  45. icon: o.icon
  46. }
  47. end
  48. pt.save!
  49. end
  50. end
  51. end

app/workers/migrate_tags_worker.rb

0.0% lines covered

24 relevant lines. 0 lines covered and 24 lines missed.
    
  1. class MigrateTagsWorker
  2. include Sidekiq::Worker
  3. def perform
  4. group_ids = []
  5. Tagging.where(taggable_type: 'Discussion').pluck(:taggable_id).uniq.each do |discussion_id|
  6. tag_ids = Tagging.where(taggable_id: discussion_id, taggable_type: 'Discussion').pluck(:tag_id)
  7. names = Tag.where(id: tag_ids).pluck(:name)
  8. if d = Discussion.find_by(id: discussion_id)
  9. group_ids.push d.group_id
  10. d.update_columns(tags: names.uniq)
  11. end
  12. end
  13. Tagging.where(taggable_type: 'Poll').pluck(:taggable_id).uniq.each do |poll_id|
  14. tag_ids = Tagging.where(taggable_id: poll_id, taggable_type: 'Poll').pluck(:tag_id)
  15. names = Tag.where(id: tag_ids).pluck(:name)
  16. if p = Poll.find_by(id: poll_id)
  17. group_ids.push p.group_id
  18. p.update_columns(tags: names.uniq)
  19. end
  20. end
  21. group_ids.each {|id| TagService.update_group_tags(id) }
  22. Group.where(id: group_ids).where(parent_id: nil).pluck(:id).each {|id| TagService.update_org_tagging_counts(id) }
  23. end
  24. end

app/workers/migrate_user_worker.rb

100.0% lines covered

35 relevant lines. 35 lines covered and 0 lines missed.
    
  1. 1 class MigrateUserWorker
  2. 1 include Sidekiq::Worker
  3. 1 attr_reader :source, :destination
  4. 1 def perform(source_id, destination_id)
  5. 2 @source = User.find_by!(id: source_id)
  6. 2 @destination = User.find_by!(id: destination_id)
  7. 2 delete_duplicates
  8. 40 operations.each { |operation| ActiveRecord::Base.connection.execute(operation) }
  9. 2 migrate_stances
  10. 2 update_counters
  11. 2 RedactUserWorker.new.perform(source_id, destination_id, false)
  12. 2 UserMailer.accounts_merged(destination.id).deliver_later
  13. end
  14. SCHEMA = {
  15. 1 attachments: :user_id,
  16. documents: :author_id,
  17. comments: :user_id,
  18. reactions: :user_id,
  19. discussion_readers: :user_id,
  20. discussions: :author_id,
  21. events: :user_id,
  22. groups: :creator_id,
  23. login_tokens: :user_id,
  24. membership_requests: [:requestor_id, :responder_id],
  25. memberships: [:user_id, :inviter_id],
  26. notifications: :user_id,
  27. oauth_applications: :owner_id,
  28. omniauth_identities: :user_id,
  29. outcomes: :author_id,
  30. polls: :author_id,
  31. versions: :whodunnit
  32. }.freeze
  33. 1 def delete_duplicates
  34. 2 Membership.delete(destination.all_memberships.
  35. joins("INNER JOIN memberships source
  36. ON source.group_id = memberships.group_id
  37. AND source.user_id = #{source.id}").pluck(:"source.id"))
  38. 2 DiscussionReader.delete(destination.discussion_readers.
  39. joins("INNER JOIN discussion_readers source
  40. ON source.discussion_id = discussion_readers.discussion_id
  41. AND source.user_id = #{source.id}").pluck(:"source.id"))
  42. end
  43. 1 def operations
  44. 2 SCHEMA.map do |table, columns|
  45. 34 Array(columns).map do |column_name|
  46. 38 "UPDATE #{table} SET #{column_name} = #{destination.id} WHERE #{column_name} = #{source.id}"
  47. end
  48. end.flatten
  49. end
  50. 1 def migrate_stances
  51. 2 Stance.where(participant: source).update_all(participant_id: destination.id, latest: false)
  52. 2 Stance.where(participant: destination).update_all(latest: false)
  53. 2 poll_ids = Stance.where(participant: destination).pluck(:poll_id).uniq
  54. 2 Poll.where(id: poll_ids).each do |poll|
  55. 1 poll.stances.where(participant: destination).order(:created_at).last.update_attribute(:latest, true)
  56. end
  57. end
  58. 1 def update_counters
  59. 2 destination.reload.groups.each do |group|
  60. 2 group.update_memberships_count
  61. 2 group.update_admin_memberships_count
  62. 2 group.update_pending_memberships_count
  63. end
  64. [
  65. 2 destination.authored_polls,
  66. destination.group_polls,
  67. destination.participated_polls
  68. ].flatten.uniq.each(&:update_counts!)
  69. 2 [source, destination].each do |user|
  70. 4 user.update_memberships_count
  71. end
  72. 2 destination.update_attribute(:sign_in_count, destination.sign_in_count + source.sign_in_count)
  73. end
  74. end

app/workers/move_comments_worker.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 class MoveCommentsWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(event_ids, source_discussion_id, target_discussion_id)
  4. 5 source_discussion = Discussion.find(source_discussion_id)
  5. 5 target_discussion = Discussion.find(target_discussion_id)
  6. # sanitize event_ids (so they cannot be from another discussion), and ensure we have any children
  7. 5 event_ids = (Event.where(id: event_ids, discussion_id: source_discussion.id).pluck(:id) +
  8. Event.where(parent_id: event_ids, discussion_id: source_discussion.id).pluck(:id)).uniq
  9. 5 all_events = Event.where(id: event_ids)
  10. 5 all_comments = Comment.where(id: Event.where(id: event_ids, eventable_type: 'Comment').pluck(:eventable_id))
  11. 5 all_polls = Poll.where(id: Event.where(id: event_ids, eventable_type: 'Poll').pluck(:eventable_id))
  12. # update eventable.discussion_id
  13. 5 all_comments.update_all(discussion_id: target_discussion.id)
  14. 5 all_polls.update_all(discussion_id: target_discussion.id)
  15. # update comment parents
  16. 5 all_comments.each do |c|
  17. 6 if c.parent.discussion_id != target_discussion_id
  18. 4 c.update_columns(parent_id: target_discussion_id, parent_type: 'Discussion')
  19. end
  20. end
  21. 5 all_events.update(discussion_id: target_discussion_id, sequence_id: nil)
  22. 5 EventService.repair_thread(target_discussion.id)
  23. 5 EventService.repair_thread(source_discussion.id)
  24. 5 SearchService.reindex_by_discussion_id(target_discussion.id)
  25. 5 SearchService.reindex_by_discussion_id(source_discussion.id)
  26. 5 ActiveStorage::Attachment.where(record: all_events.map(&:eventable).compact).update_all(group_id: target_discussion.group_id)
  27. 5 MessageChannelService.publish_models(target_discussion.items, group_id: target_discussion.group.id)
  28. end
  29. end

app/workers/publish_event_worker.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class PublishEventWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(event_id)
  4. 994 Event.sti_find(event_id).trigger!
  5. end
  6. end

app/workers/redact_user_worker.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class RedactUserWorker
  2. 1 include Sidekiq::Worker
  3. # we deactivate and redact the user
  4. 1 def perform(user_id, actor_id, send_email = true)
  5. 6 user = User.find_by!(id:user_id)
  6. 6 return if user.email.nil?
  7. 6 email = user.email
  8. 6 locale = user.locale
  9. 6 deactivated_at = user.deactivated_at || DateTime.now
  10. 6 group_ids = Membership.active.where(user_id: user_id).pluck(:group_id)
  11. 6 user.uploaded_avatar.purge_later
  12. 6 User.transaction do
  13. 6 MembershipService.revoke_by_id(group_ids, user_id, actor_id, deactivated_at)
  14. 6 User.where(id: user_id).update_all(
  15. is_admin: false,
  16. api_key: nil,
  17. secret_token: nil,
  18. name: nil,
  19. email: nil,
  20. short_bio: '',
  21. username: nil,
  22. experiences: {},
  23. avatar_kind: "initials",
  24. avatar_initials: nil,
  25. country: nil,
  26. region: nil,
  27. city: nil,
  28. location: '',
  29. email_newsletter: false,
  30. unlock_token: nil,
  31. current_sign_in_ip: nil,
  32. last_sign_in_ip: nil,
  33. encrypted_password: nil,
  34. reset_password_token: nil,
  35. reset_password_sent_at: nil,
  36. unsubscribe_token: nil,
  37. detected_locale: nil,
  38. email_verified: false,
  39. legal_accepted_at: nil,
  40. # set an email_sha256 so we can identify redacted accounts if someone provides an email
  41. email_sha256: Digest::SHA256.hexdigest(email),
  42. deactivated_at: deactivated_at,
  43. deactivator_id: actor_id
  44. )
  45. 6 PaperTrail::Version.where(item_type: 'User', item_id: user_id).delete_all
  46. 6 Identities::Base.where(user_id: user_id).delete_all
  47. 6 MembershipRequest.where(requestor_id: user_id, responded_at: nil).delete_all
  48. end
  49. 6 NewsletterService.unsubscribe(email)
  50. 6 UserMailer.redacted(email, locale).deliver_later if send_email
  51. 6 SearchService.reindex_by_author_id(user.id)
  52. end
  53. end

app/workers/remove_poll_expired_from_threads_worker.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. class RemovePollExpiredFromThreadsWorker
  2. include Sidekiq::Worker
  3. sidekiq_options queue: :low, retry: false
  4. def perform(poll_id)
  5. p = Poll.find(poll_id)
  6. count = Event.where(eventable: p, kind: 'poll_expired').where("discussion_id is not null").
  7. update_all(discussion_id: nil, sequence_id: nil, position: 0, position_key: nil)
  8. EventService.repair_thread(p.discussion_id) if count > 0
  9. puts "count: #{count}, poll_id: #{poll_id}, discussion_id: #{p.discussion_id}"
  10. end
  11. end

app/workers/repair_thread_worker.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class RepairThreadWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options retry: false
  4. 1 def perform(discussion_id)
  5. 4 EventService.repair_thread(discussion_id)
  6. end
  7. end

app/workers/reset_poll_stance_data_worker.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. class ResetPollStanceDataWorker
  2. include Sidekiq::Worker
  3. sidekiq_options queue: :low, retry: false
  4. def perform(poll_id)
  5. p = Poll.find(poll_id)
  6. # p.reset_latest_stances!
  7. p.stances.each(&:update_option_scores!)
  8. p.update_counts!
  9. end
  10. end

app/workers/revoke_memberships_of_deactivated_users_worker.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. class RevokeMembershipsOfDeactivatedUsersWorker
  2. include Sidekiq::Worker
  3. def perform
  4. User.where.not(deactivated_at: nil).find_each do |user|
  5. group_ids = Membership.active.where(user_id: user.id).pluck(:group_id)
  6. if group_ids.count > 0
  7. MembershipService.revoke_by_id(group_ids, user.id, user.id, user.deactivated_at)
  8. end
  9. end
  10. end
  11. end

app/workers/send_daily_catch_up_email_worker.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. 1 class SendDailyCatchUpEmailWorker
  2. 1 include Sidekiq::Worker
  3. 1 sidekiq_options retry: false
  4. 1 def perform
  5. 5 User.distinct.pluck(:time_zone).uniq.each do |zone|
  6. 5 if Time.find_zone(zone)
  7. 5 time_in_zone = DateTime.now.in_time_zone(zone)
  8. 5 if time_in_zone.hour == 6
  9. 4 days = [7, time_in_zone.wday, (time_in_zone.wday % 2 == 1) ? 8 : nil].compact
  10. 4 User.distinct.active.verified.where(time_zone: zone).where(email_catch_up_day: days).find_each do |user|
  11. 2 period = case user.email_catch_up_day
  12. when 8 then 'other'
  13. 1 when 7 then 'daily'
  14. else
  15. 1 'weekly'
  16. end
  17. 2 UserMailer.catch_up(user.id, nil, period).deliver_now
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/workers/update_blocked_domains_worker.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. class UpdateBlockedDomainsWorker
  2. include Sidekiq::Worker
  3. def perform
  4. puts "updating blocked domains"
  5. hostsfile = 'https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts'
  6. BlockedDomain.delete_all
  7. URI.open(hostsfile, 'r').each do |line|
  8. next unless line.starts_with?('0.0.0.0 ')
  9. domain = line.split(" ")[1]
  10. BlockedDomain.create(name: domain)
  11. end
  12. puts "updating blocked domains completed"
  13. end
  14. end

app/workers/update_poll_counts_worker.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. class UpdatePollCountsWorker
  2. include Sidekiq::Worker
  3. sidekiq_options queue: :low, retry: false
  4. def perform(poll_id)
  5. p = Poll.find(poll_id)
  6. p.update_counts!
  7. end
  8. end

app/workers/update_tag_worker.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 class UpdateTagWorker
  2. 1 include Sidekiq::Worker
  3. 1 def perform(group_id, old_name, new_name, color)
  4. 5 group = Group.find(group_id)
  5. 5 group_ids = group.id_and_subgroup_ids
  6. 5 if old_name != new_name
  7. 5 Tag.where(group_id: group_ids, name: new_name).delete_all
  8. 5 Tag.where(group_id: group_ids, name: old_name).update_all(name: new_name)
  9. end
  10. 5 Discussion.where(group_id: group_ids).where.contains(tags: [old_name]).find_each do |d|
  11. 8 d.tags[d.tags.index(old_name)] = new_name
  12. 8 d.update_column(:tags, d.tags.uniq)
  13. end
  14. 5 Poll.where(group_id: group_ids).where.contains(tags: [old_name]).find_each do |p|
  15. 8 p.tags[p.tags.index(old_name)] = new_name
  16. 8 p.update_column(:tags, p.tags.uniq)
  17. end
  18. 5 group_ids.each do |group_id|
  19. 8 TagService.update_group_tags(group_id)
  20. end
  21. 5 Tag.where(group_id: group_ids, name: new_name).update_all(color: color)
  22. 5 TagService.update_org_tagging_counts(group.parent_or_self.id)
  23. end
  24. end

lib/analyzers/transcription_analyzer.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. # https://www.modern-rails.com/posts/activestorage-analyzers-and-the-openai-transcription-api/
  2. class TranscriptionAnalyzer < ActiveStorage::Analyzer::AudioAnalyzer
  3. def metadata
  4. super.merge(text: @text, language: @language).compact
  5. end
  6. private
  7. def probe_from(file)
  8. super.tap do
  9. response = TranscriptionService.transcribe(file)
  10. @text = response["text"]
  11. @language = response["language"]
  12. record = blob.attachments.first.record
  13. # record.body += "<p>#{I18n.t('record_modal.audio_transcript', text: @text, locale: record.author.locale)}</p>"
  14. record.body += "<p>#{@text}</p>"
  15. record.save!
  16. MessageChannelService.publish_models(Array(record), group_id: record.group_id, user_id: record.author_id)
  17. end
  18. end
  19. end

lib/event_bus.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. class EventBus
  2. def self.configure
  3. yield self
  4. end
  5. def self.broadcast(event, *params)
  6. listeners[event].each { |listener| listener.call(*params) }
  7. end
  8. def self.listen(*events, &block)
  9. events.each { |event| listeners[event].add(block) }
  10. end
  11. def self.deafen(*events, &block)
  12. events.each { |event| listeners[event].delete(block) }
  13. end
  14. def self.clear
  15. @@listeners = nil
  16. end
  17. def self.listeners
  18. @@listeners ||= Hash.new { |hash, key| hash[key] = Set.new }
  19. end
  20. private_class_method :listeners
  21. end

lib/pie_chart.rb

0.0% lines covered

46 relevant lines. 0 lines covered and 46 lines missed.
    
  1. require 'victor'
  2. class PieChartSVG
  3. SIZE = 512
  4. def self.from_primitives(scores, colors)
  5. slices = []
  6. scores.each_with_index do |v, i|
  7. slices << {value: (v.to_f / scores.sum) * 100, color: colors[i]}
  8. end
  9. draw(slices)
  10. end
  11. def self.from_poll(p)
  12. draw(pie_slices(p))
  13. end
  14. def self.arc_path(start_angle, end_angle)
  15. radius = SIZE/2
  16. rad = Math::PI / 180;
  17. x1 = radius + (radius * Math.cos(-start_angle * rad));
  18. x2 = radius + (radius * Math.cos(-end_angle * rad));
  19. y1 = radius + (radius * Math.sin(-start_angle * rad));
  20. y2 = radius + (radius * Math.sin(-end_angle * rad));
  21. return ["M", radius, radius, "L", x1, y1, "A", radius, radius, 0, (((end_angle - start_angle) > 180) ? 1 : 0), 0, x2, y2, "z"].join(' ');
  22. end
  23. def self.pie_slices(poll)
  24. results = PollService.calculate_results(poll, poll.poll_options)
  25. results.filter { |r| r[poll.chart_column] }.map do |r|
  26. {value: r[poll.chart_column], color: r[:color] }
  27. end
  28. end
  29. def self.draw(slices)
  30. svg = Victor::SVG.new(width: SIZE, height: SIZE)
  31. case slices.length
  32. when 0
  33. svg.circle cx: SIZE/2, cy: SIZE/2, r: SIZE/2, fill: 'grey'
  34. when 1
  35. svg.circle cx: SIZE/2, cy: SIZE/2, r: SIZE/2, fill: slices[0][:color]
  36. else
  37. start = 90
  38. slices.each do |slice|
  39. angle = (360 * slice[:value]) / 100
  40. svg.path d: arc_path(start, start+angle), stroke_width: 0, fill: slice[:color]
  41. start += angle
  42. end
  43. end
  44. svg
  45. end
  46. end

lib/slack_mrkdwn.rb

0.0% lines covered

115 relevant lines. 0 lines covered and 115 lines missed.
    
  1. # frozen_string_literal: true
  2. # thank you to the author! https://github.com/BlazingBBQ/SlackMrkdwn
  3. require 'redcarpet'
  4. class SlackMrkdwn < Redcarpet::Render::Base
  5. class << self
  6. def from(markdown)
  7. renderer = SlackMrkdwn.new
  8. Redcarpet::Markdown.new(renderer, strikethrough: true, underline: true, fenced_code_blocks: true).render(markdown)
  9. end
  10. end
  11. # Methods where the first argument is the text content
  12. [
  13. # block-level calls
  14. :block_html,
  15. :autolink,
  16. :raw_html,
  17. :table, :table_row, :table_cell,
  18. :superscript, :highlight,
  19. # footnotes
  20. :footnotes, :footnote_def, :footnote_ref,
  21. :hrule,
  22. # low level rendering
  23. :entity, :normal_text,
  24. :doc_header, :doc_footer,
  25. ].each do |method|
  26. define_method method do |*args|
  27. args.first
  28. end
  29. end
  30. # Encode Slack restricted characters
  31. def preprocess(content)
  32. content.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
  33. end
  34. def postprocess(content)
  35. content.rstrip
  36. end
  37. # ~~strikethrough~~
  38. def strikethrough(content)
  39. "~#{content}~"
  40. end
  41. # _italic_
  42. def underline(content)
  43. "_#{content}_"
  44. end
  45. # *italic*
  46. def emphasis(content)
  47. "_#{content}_"
  48. end
  49. # **bold**
  50. def double_emphasis(content)
  51. "*#{content}*"
  52. end
  53. # ***bold and italic***
  54. def triple_emphasis(content)
  55. "*_#{content}_*"
  56. end
  57. # ``` code block ```
  58. def block_code(content, _language)
  59. "```\n#{content}```\n\n"
  60. end
  61. # > quote
  62. def block_quote(content)
  63. "&gt; #{content}"
  64. end
  65. # `code`
  66. def codespan(content)
  67. "`#{content}`"
  68. end
  69. # links
  70. def link(link, _title, content)
  71. "<#{link}|#{content}>"
  72. end
  73. # list. Called when all list items have been consumed
  74. def list(entries, style)
  75. entries = format_list(entries, style)
  76. remember_last_list_entries(entries)
  77. entries
  78. end
  79. # list item
  80. def list_item(entry, _style)
  81. if @last_entries && entry.end_with?(@last_entries)
  82. entry = indent_list_items(entry)
  83. @last_entries = nil
  84. end
  85. entry
  86. end
  87. # ![](image)
  88. def image(url, _title, _content)
  89. link(url, _title, File.basename(url))
  90. end
  91. def paragraph(text)
  92. pre_spacing = @last_entries ? "\n" : nil
  93. clear_last_list_entries
  94. "#{pre_spacing}#{text}\n\n"
  95. end
  96. # # Header
  97. def header(text, _header_level)
  98. "*#{text}*\n"
  99. end
  100. def linebreak()
  101. "\n"
  102. end
  103. private
  104. def format_list(entries, style)
  105. case style
  106. when :ordered
  107. number_list(entries)
  108. when :unordered
  109. add_dashes(entries)
  110. end
  111. end
  112. def add_dashes(entries)
  113. entries.gsub(/^(\S+.*)$/, '- \1')
  114. end
  115. def number_list(entries)
  116. count = 0
  117. entries.gsub(/^(\S+.*)$/) do
  118. match = Regexp.last_match
  119. count += 1
  120. "#{count}. #{match[0]}"
  121. end
  122. end
  123. def remember_last_list_entries(entries)
  124. @last_entries = entries
  125. end
  126. def clear_last_list_entries
  127. @last_entries = nil
  128. end
  129. def nest_list_entries(entries)
  130. entries.gsub(/^(.+)$/, ' \1')
  131. end
  132. def indent_list_items(entry)
  133. entry.gsub(@last_entries, nest_list_entries(@last_entries))
  134. end
  135. end

lib/version.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module Loomio
  2. module Version
  3. def self.current
  4. "2.24.0"
  5. end
  6. end
  7. end